blob: 8dc6e31234a7f41183841c0eb3d5d0531d2bbf1d [file] [log] [blame]
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Displays options to build an intent with different configurations of flags
* and target activities, and allows the user to launch an activity with the built intent.
*/
public class IntentBuilderView extends FrameLayout implements View.OnClickListener,
CompoundButton.OnCheckedChangeListener {
private static final String TAG = "IntentBuilderView";
protected final int TAG_FLAG = R.id.tag_flag;
protected final int TAG_SUGGESTED = R.id.tag_suggested;
protected ComponentName mActivityToLaunch;
private boolean mVerifyMode;
private ColorStateList mSuggestTint;
private ColorStateList mDefaultTint;
private LinearLayout mLayout;
private Context mContext;
private LayoutInflater mInflater;
private List<RadioButton> mRadioButtons;
/**
* Constructs a new IntentBuilderView, in the specified mode.
*
* @param context The context of the activity that holds this view.
* @param mode The mode to launch in (if null, default mode turns suggestions off). Passing
* {@link BaseActivity.Mode} will turn on suggestions
* by default.
*/
public IntentBuilderView(@NonNull Context context, BaseActivity.Mode mode) {
super(context);
mContext = context;
mInflater = LayoutInflater.from(context);
mLayout = (LinearLayout) mInflater.inflate(R.layout.view_build_intent,
this /* root */, false /* attachToRoot */);
addView(mLayout, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
mActivityToLaunch = new ComponentName(context,
TaskAffinity1Activity.class);
mSuggestTint = context.getColorStateList(R.color.suggested_checkbox);
mDefaultTint = context.getColorStateList(R.color.default_checkbox);
mVerifyMode = mode != null && mode == BaseActivity.Mode.VERIFY;
setTag(BaseActivity.BUILDER_VIEW);
setId(R.id.build_intent_container);
setBackground(context.getResources().getDrawable(R.drawable.card_background,
null /*theme*/));
setupViews();
}
private Class<?> getClass(String name) {
String fullName = mContext.getPackageName().concat(".").concat(name);
try {
return Class.forName(fullName);
} catch (ClassNotFoundException e) {
if (BuildConfig.DEBUG) e.printStackTrace();
throw new RuntimeException(e);
}
}
private void setupViews() {
PackageInfo packInfo;
// Retrieve activities and their manifest flags
PackageManager pm = mContext.getPackageManager();
try {
packInfo = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
Toast.makeText(mContext,
"Cannot find activities, this should never happen " + e.toString(),
Toast.LENGTH_SHORT).show();
throw new RuntimeException(e);
}
List<ActivityInfo> activities = Arrays.asList(packInfo.activities);
Map<ActivityInfo, List<String>> activityToFlags = new HashMap<>();
activities.forEach(activityInfo ->
activityToFlags.put(activityInfo, FlagUtils.getActivityFlags(activityInfo)));
// Get handles to views
LinearLayout flagBuilderLayout = mLayout.findViewById(R.id.build_intent_flags);
RadioGroup activityRadios = mLayout.findViewById(R.id.radioGroup_launchMode);
// Populate views with text
fillCheckBoxLayout(flagBuilderLayout, FlagUtils.intentFlagsByCategory(),
R.layout.section_header, R.id.header_title, R.layout.checkbox_list_item,
R.id.checkBox_item);
// Add radios for activity combos
List<RadioButton> radioButtons = new ArrayList<>();
activityToFlags.entrySet().stream()
.sorted(Comparator.comparing(
activityEntry -> nameOfActivityInfo(activityEntry.getKey())))
.forEach(activityEntry -> {
ActivityInfo activityInfo = activityEntry.getKey();
List<String> manifestFlags = activityEntry.getValue();
LinearLayout actRadio = (LinearLayout) mInflater
.inflate(R.layout.activity_radio_list_item, null /* root */);
RadioButton rb = actRadio.findViewById(R.id.radio_launchMode);
rb.setText(activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1));
rb.setTag(activityInfo);
((TextView) actRadio.findViewById(R.id.activity_desc)).setText(
manifestFlags.stream().collect(Collectors.joining("\n")));
rb.setOnClickListener(this);
activityRadios.addView(actRadio);
radioButtons.add(rb);
});
((CompoundButton) mLayout.findViewById(R.id.suggestion_switch))
.setOnCheckedChangeListener(this);
mRadioButtons = radioButtons;
}
private String nameOfActivityInfo(ActivityInfo activityInfo) {
return activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1);
}
/**
* Fills the {@link ViewGroup} with a list separated by section
*
* @param layout The layout to fill
* @param categories A map of category names to list items within that category
* @param categoryLayoutRes the layout resource of the category header view
* @param categoryViewId the resource id of the category {@link TextView} within the layout
* @param itemLayoutRes the layout resource of the list item view
* @param itemViewId the resource id of the item {@link TextView} within the item layout
*/
private void fillCheckBoxLayout(ViewGroup layout, Map<String, List<String>> categories,
int categoryLayoutRes, int categoryViewId, int itemLayoutRes, int itemViewId) {
layout.removeAllViews();
for (String category : categories.keySet()) {
View categoryLayout = mInflater.inflate(categoryLayoutRes, layout,
false /* attachToRoot */);
TextView categoryView = categoryLayout.findViewById(categoryViewId);
categoryView.setText(category);
layout.addView(categoryLayout);
for (String item : categories.get(category)) {
View itemLayout = mInflater.inflate(itemLayoutRes, layout,
false /* attachToRoot */);
CheckBox itemView = itemLayout.findViewById(itemViewId);
IntentFlag flag = FlagUtils.getFlagForString(item);
itemView.setTag(TAG_FLAG, flag);
itemView.setText(item);
itemView.setOnCheckedChangeListener(this);
layout.addView(itemLayout);
}
}
}
@Override
public void onClick(View view) {
// Handles selection of target activity
if (view instanceof RadioButton) {
ActivityInfo tag = (ActivityInfo) view.getTag();
mActivityToLaunch = new ComponentName(mContext,
getClass(tag.name.substring(tag.name.lastIndexOf(".") + 1)));
mRadioButtons.stream().filter(rb -> rb != view)
.forEach(rb -> rb.setChecked(false));
}
}
public Intent currentIntent() {
LinearLayout flagBuilder = mLayout.findViewById(R.id.build_intent_flags);
Intent intent = new Intent();
// Gather flags from flag builder checkbox list
childrenOfGroup(flagBuilder, CheckBox.class)
.forEach(checkbox -> {
int flagVal = FlagUtils.flagValue(checkbox.getText().toString());
if (checkbox.isChecked()) {
intent.addFlags(flagVal);
} else {
intent.removeFlags(flagVal);
}
});
intent.setComponent(mActivityToLaunch);
return intent;
}
public boolean startForResult() {
RadioButton startNormal = mLayout.findViewById(R.id.start_normal);
return !startNormal.isChecked();
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
int buttonId = compoundButton.getId();
if (buttonId == R.id.checkBox_item) {
// A checkbox was checked/unchecked
IntentFlag flag = (IntentFlag) compoundButton.getTag(TAG_FLAG);
if (flag != null && mVerifyMode) {
refreshConstraints();
if (checked) {
suggestFlags(flag);
selectFlags(flag.getRequests());
} else {
clearSuggestions();
}
}
} else if (buttonId == R.id.suggestion_switch) {
// Suggestions were turned on/off
clearSuggestions();
mVerifyMode = checked;
if (mVerifyMode) {
refreshConstraints();
getCheckedFlags().forEach(this::suggestFlags);
} else {
enableAllFlags();
}
}
}
private void refreshConstraints() {
enableAllFlags();
getCheckedFlags().forEach(flag -> disableFlags(flag.getConflicts()));
}
private void suggestFlags(IntentFlag flag) {
clearSuggestions();
List<String> suggestions = flag.getComplements().stream().map(IntentFlag::getName)
.collect(Collectors.toList());
getAllCheckBoxes().stream()
.filter(box -> hasSuggestion(suggestions, box))
.forEach(box -> {
box.setButtonTintList(mSuggestTint);
box.setTag(TAG_SUGGESTED, true);
});
}
private boolean hasSuggestion(List<String> suggestions, CheckBox box) {
IntentFlag flag = (IntentFlag) box.getTag(TAG_FLAG);
if (flag != null) {
return suggestions.contains(flag.getName());
} else {
Log.w(TAG, "Unknown flag: " + box.getText());
return false;
}
}
private void clearSuggestions() {
getAllCheckBoxes().forEach(box -> box.setButtonTintList(mDefaultTint));
}
/**
* Clears all of the checkboxes in this builder.
*/
public void clearFlags() {
getAllCheckBoxes().forEach(box -> box.setChecked(false));
}
private List<CheckBox> getAllCheckBoxes() {
View layout = mLayout;
ViewGroup flagBuilder = (LinearLayout) layout.findViewById(R.id.build_intent_flags);
List<CheckBox> checkBoxes = new LinkedList<>();
for (int i = 0; i < flagBuilder.getChildCount(); i++) {
View child = flagBuilder.getChildAt(i);
if (child instanceof CheckBox) {
checkBoxes.add((CheckBox) child);
}
}
return checkBoxes;
}
/**
* Retrieve children of a certain type from a {@link ViewGroup}.
*
* @param group the ViewGroup to retrieve children from.
*/
protected static <T> List<T> childrenOfGroup(ViewGroup group, Class<T> viewType) {
List<T> list = new LinkedList<>();
for (int i = 0; i < group.getChildCount(); i++) {
View v = group.getChildAt(i);
if (viewType.isAssignableFrom(v.getClass())) list.add(viewType.cast(v));
}
return list;
}
/**
* Selects the checkboxes for the given list of flags.
*
* @param flags A list of mIntent flags to select.
*/
public void selectFlags(List<String> flags) {
getAllCheckBoxes().forEach(box -> {
if (flags.contains(box.getText())) {
box.setChecked(true);
}
});
}
/**
* Selects the checkboxes for the given list of flags.
*
* @param flags A list of mIntent flags to select.
*/
public void selectFlags(Collection<IntentFlag> flags) {
selectFlags(flags.stream().map(IntentFlag::getName).collect(Collectors.toList()));
}
private void enableAllFlags() {
getAllCheckBoxes().forEach(box -> box.setEnabled(true));
}
private Collection<CheckBox> getChecked() {
return getAllCheckBoxes().stream().filter(CompoundButton::isChecked)
.collect(Collectors.toList());
}
private Collection<IntentFlag> getCheckedFlags() {
return getChecked().stream().map(checkBox -> (IntentFlag) checkBox.getTag(TAG_FLAG))
.collect(Collectors.toList());
}
private void disableFlags(Collection<IntentFlag> flags) {
flags.forEach(flag -> getCheckBox(flag).setEnabled(false));
}
private CheckBox getCheckBox(IntentFlag flag) {
return getAllCheckBoxes().stream().filter(box -> flag.getName().equals(box.getText()))
.findFirst().orElse(null);
}
/**
* A functional interface that represents the action to take upon the user pressing the launch
* button within this view.
*/
public interface OnLaunchCallback {
void launchActivity(Intent intent, boolean forResult);
}
}