Intent builderview currently had a hierarchy consiting of: RadioGroup -> LinearLayout -> RadioButton. However if the RadioButton is not directly inside the RadioGroup, the RadioGroup has no effect on the button. Instead keep a list of all the RadioButtons we have and deselect all others when a new button gets clicked. Test: Manual Change-Id: I65b9e1165ae3abc23b10f7eb8770a60f144aac82
376 lines
15 KiB
Java
376 lines
15 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
}
|