Home | History | Annotate | Download | only in intentplayground
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.intentplayground;
     18 
     19 import android.content.ComponentName;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.pm.ActivityInfo;
     23 import android.content.pm.PackageInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.res.ColorStateList;
     26 import android.util.Log;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.widget.CheckBox;
     31 import android.widget.CompoundButton;
     32 import android.widget.FrameLayout;
     33 import android.widget.LinearLayout;
     34 import android.widget.RadioButton;
     35 import android.widget.RadioGroup;
     36 import android.widget.TextView;
     37 import android.widget.Toast;
     38 
     39 import androidx.annotation.NonNull;
     40 
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.Collection;
     44 import java.util.Comparator;
     45 import java.util.HashMap;
     46 import java.util.LinkedList;
     47 import java.util.List;
     48 import java.util.Map;
     49 import java.util.stream.Collectors;
     50 
     51 /**
     52  * Displays options to build an intent with different configurations of flags
     53  * and target activities, and allows the user to launch an activity with the built intent.
     54  */
     55 public class IntentBuilderView extends FrameLayout implements View.OnClickListener,
     56         CompoundButton.OnCheckedChangeListener {
     57     private static final String TAG = "IntentBuilderView";
     58     protected final int TAG_FLAG = R.id.tag_flag;
     59     protected final int TAG_SUGGESTED = R.id.tag_suggested;
     60     protected ComponentName mActivityToLaunch;
     61     private boolean mVerifyMode;
     62     private ColorStateList mSuggestTint;
     63     private ColorStateList mDefaultTint;
     64     private LinearLayout mLayout;
     65     private Context mContext;
     66     private LayoutInflater mInflater;
     67     private List<RadioButton> mRadioButtons;
     68 
     69     /**
     70      * Constructs a new IntentBuilderView, in the specified mode.
     71      *
     72      * @param context The context of the activity that holds this view.
     73      * @param mode    The mode to launch in (if null, default mode turns suggestions off). Passing
     74      *                {@link BaseActivity.Mode} will turn on suggestions
     75      *                by default.
     76      */
     77     public IntentBuilderView(@NonNull Context context, BaseActivity.Mode mode) {
     78         super(context);
     79         mContext = context;
     80         mInflater = LayoutInflater.from(context);
     81         mLayout = (LinearLayout) mInflater.inflate(R.layout.view_build_intent,
     82                 this /* root */, false /* attachToRoot */);
     83         addView(mLayout, new LayoutParams(LayoutParams.MATCH_PARENT,
     84                 LayoutParams.MATCH_PARENT));
     85         mActivityToLaunch = new ComponentName(context,
     86                 TaskAffinity1Activity.class);
     87         mSuggestTint = context.getColorStateList(R.color.suggested_checkbox);
     88         mDefaultTint = context.getColorStateList(R.color.default_checkbox);
     89         mVerifyMode = mode != null && mode == BaseActivity.Mode.VERIFY;
     90         setTag(BaseActivity.BUILDER_VIEW);
     91         setId(R.id.build_intent_container);
     92         setBackground(context.getResources().getDrawable(R.drawable.card_background,
     93                 null /*theme*/));
     94         setupViews();
     95     }
     96 
     97     private Class<?> getClass(String name) {
     98         String fullName = mContext.getPackageName().concat(".").concat(name);
     99         try {
    100             return Class.forName(fullName);
    101         } catch (ClassNotFoundException e) {
    102             if (BuildConfig.DEBUG) e.printStackTrace();
    103             throw new RuntimeException(e);
    104         }
    105     }
    106 
    107     private void setupViews() {
    108         PackageInfo packInfo;
    109 
    110         // Retrieve activities and their manifest flags
    111         PackageManager pm = mContext.getPackageManager();
    112         try {
    113             packInfo = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
    114         } catch (PackageManager.NameNotFoundException e) {
    115             Toast.makeText(mContext,
    116                     "Cannot find activities, this should never happen " + e.toString(),
    117                     Toast.LENGTH_SHORT).show();
    118             throw new RuntimeException(e);
    119         }
    120         List<ActivityInfo> activities = Arrays.asList(packInfo.activities);
    121         Map<ActivityInfo, List<String>> activityToFlags = new HashMap<>();
    122         activities.forEach(activityInfo ->
    123                 activityToFlags.put(activityInfo, FlagUtils.getActivityFlags(activityInfo)));
    124 
    125         // Get handles to views
    126         LinearLayout flagBuilderLayout = mLayout.findViewById(R.id.build_intent_flags);
    127         RadioGroup activityRadios = mLayout.findViewById(R.id.radioGroup_launchMode);
    128         // Populate views with text
    129         fillCheckBoxLayout(flagBuilderLayout, FlagUtils.intentFlagsByCategory(),
    130                 R.layout.section_header, R.id.header_title, R.layout.checkbox_list_item,
    131                 R.id.checkBox_item);
    132 
    133         // Add radios for activity combos
    134         List<RadioButton> radioButtons = new ArrayList<>();
    135         activityToFlags.entrySet().stream()
    136                 .sorted(Comparator.comparing(
    137                         activityEntry -> nameOfActivityInfo(activityEntry.getKey())))
    138                 .forEach(activityEntry -> {
    139                     ActivityInfo activityInfo = activityEntry.getKey();
    140                     List<String> manifestFlags = activityEntry.getValue();
    141 
    142                     LinearLayout actRadio = (LinearLayout) mInflater
    143                             .inflate(R.layout.activity_radio_list_item, null /* root */);
    144                     RadioButton rb = actRadio.findViewById(R.id.radio_launchMode);
    145                     rb.setText(activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1));
    146                     rb.setTag(activityInfo);
    147                     ((TextView) actRadio.findViewById(R.id.activity_desc)).setText(
    148                             manifestFlags.stream().collect(Collectors.joining("\n")));
    149                     rb.setOnClickListener(this);
    150                     activityRadios.addView(actRadio);
    151                     radioButtons.add(rb);
    152                 });
    153         ((CompoundButton) mLayout.findViewById(R.id.suggestion_switch))
    154                 .setOnCheckedChangeListener(this);
    155         mRadioButtons = radioButtons;
    156     }
    157 
    158 
    159     private String nameOfActivityInfo(ActivityInfo activityInfo) {
    160         return activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1);
    161     }
    162 
    163     /**
    164      * Fills the {@link ViewGroup} with a list separated by section
    165      *
    166      * @param layout            The layout to fill
    167      * @param categories        A map of category names to list items within that category
    168      * @param categoryLayoutRes the layout resource of the category header view
    169      * @param categoryViewId    the resource id of the category {@link TextView} within the layout
    170      * @param itemLayoutRes     the layout resource of the list item view
    171      * @param itemViewId        the resource id of the item {@link TextView} within the item layout
    172      */
    173     private void fillCheckBoxLayout(ViewGroup layout, Map<String, List<String>> categories,
    174             int categoryLayoutRes, int categoryViewId, int itemLayoutRes, int itemViewId) {
    175         layout.removeAllViews();
    176         for (String category : categories.keySet()) {
    177             View categoryLayout = mInflater.inflate(categoryLayoutRes, layout,
    178                     false /* attachToRoot */);
    179             TextView categoryView = categoryLayout.findViewById(categoryViewId);
    180             categoryView.setText(category);
    181             layout.addView(categoryLayout);
    182             for (String item : categories.get(category)) {
    183                 View itemLayout = mInflater.inflate(itemLayoutRes, layout,
    184                         false /* attachToRoot */);
    185                 CheckBox itemView = itemLayout.findViewById(itemViewId);
    186                 IntentFlag flag = FlagUtils.getFlagForString(item);
    187                 itemView.setTag(TAG_FLAG, flag);
    188                 itemView.setText(item);
    189                 itemView.setOnCheckedChangeListener(this);
    190                 layout.addView(itemLayout);
    191             }
    192         }
    193     }
    194 
    195     @Override
    196     public void onClick(View view) {
    197         // Handles selection of target activity
    198         if (view instanceof RadioButton) {
    199             ActivityInfo tag = (ActivityInfo) view.getTag();
    200             mActivityToLaunch = new ComponentName(mContext,
    201                     getClass(tag.name.substring(tag.name.lastIndexOf(".") + 1)));
    202             mRadioButtons.stream().filter(rb -> rb != view)
    203                     .forEach(rb -> rb.setChecked(false));
    204         }
    205     }
    206 
    207     public Intent currentIntent() {
    208         LinearLayout flagBuilder = mLayout.findViewById(R.id.build_intent_flags);
    209         Intent intent = new Intent();
    210         // Gather flags from flag builder checkbox list
    211         childrenOfGroup(flagBuilder, CheckBox.class)
    212                 .forEach(checkbox -> {
    213                     int flagVal = FlagUtils.flagValue(checkbox.getText().toString());
    214                     if (checkbox.isChecked()) {
    215                         intent.addFlags(flagVal);
    216                     } else {
    217                         intent.removeFlags(flagVal);
    218                     }
    219                 });
    220         intent.setComponent(mActivityToLaunch);
    221         return intent;
    222     }
    223 
    224 
    225     public boolean startForResult() {
    226         RadioButton startNormal = mLayout.findViewById(R.id.start_normal);
    227         return !startNormal.isChecked();
    228     }
    229 
    230     @Override
    231     public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
    232         int buttonId = compoundButton.getId();
    233         if (buttonId == R.id.checkBox_item) {
    234             // A checkbox was checked/unchecked
    235             IntentFlag flag = (IntentFlag) compoundButton.getTag(TAG_FLAG);
    236             if (flag != null && mVerifyMode) {
    237                 refreshConstraints();
    238                 if (checked) {
    239                     suggestFlags(flag);
    240                     selectFlags(flag.getRequests());
    241                 } else {
    242                     clearSuggestions();
    243                 }
    244             }
    245         } else if (buttonId == R.id.suggestion_switch) {
    246             // Suggestions were turned on/off
    247             clearSuggestions();
    248             mVerifyMode = checked;
    249             if (mVerifyMode) {
    250                 refreshConstraints();
    251                 getCheckedFlags().forEach(this::suggestFlags);
    252             } else {
    253                 enableAllFlags();
    254             }
    255         }
    256     }
    257 
    258     private void refreshConstraints() {
    259         enableAllFlags();
    260         getCheckedFlags().forEach(flag -> disableFlags(flag.getConflicts()));
    261     }
    262 
    263     private void suggestFlags(IntentFlag flag) {
    264         clearSuggestions();
    265         List<String> suggestions = flag.getComplements().stream().map(IntentFlag::getName)
    266                 .collect(Collectors.toList());
    267         getAllCheckBoxes().stream()
    268                 .filter(box -> hasSuggestion(suggestions, box))
    269                 .forEach(box -> {
    270                     box.setButtonTintList(mSuggestTint);
    271                     box.setTag(TAG_SUGGESTED, true);
    272                 });
    273     }
    274 
    275     private boolean hasSuggestion(List<String> suggestions, CheckBox box) {
    276         IntentFlag flag = (IntentFlag) box.getTag(TAG_FLAG);
    277         if (flag != null) {
    278             return suggestions.contains(flag.getName());
    279         } else {
    280             Log.w(TAG, "Unknown flag: " + box.getText());
    281             return false;
    282         }
    283     }
    284 
    285     private void clearSuggestions() {
    286         getAllCheckBoxes().forEach(box -> box.setButtonTintList(mDefaultTint));
    287     }
    288 
    289     /**
    290      * Clears all of the checkboxes in this builder.
    291      */
    292     public void clearFlags() {
    293         getAllCheckBoxes().forEach(box -> box.setChecked(false));
    294     }
    295 
    296     private List<CheckBox> getAllCheckBoxes() {
    297         View layout = mLayout;
    298         ViewGroup flagBuilder = (LinearLayout) layout.findViewById(R.id.build_intent_flags);
    299         List<CheckBox> checkBoxes = new LinkedList<>();
    300         for (int i = 0; i < flagBuilder.getChildCount(); i++) {
    301             View child = flagBuilder.getChildAt(i);
    302             if (child instanceof CheckBox) {
    303                 checkBoxes.add((CheckBox) child);
    304             }
    305         }
    306         return checkBoxes;
    307     }
    308 
    309     /**
    310      * Retrieve children of a certain type from a {@link ViewGroup}.
    311      *
    312      * @param group the ViewGroup to retrieve children from.
    313      */
    314     protected static <T> List<T> childrenOfGroup(ViewGroup group, Class<T> viewType) {
    315         List<T> list = new LinkedList<>();
    316         for (int i = 0; i < group.getChildCount(); i++) {
    317             View v = group.getChildAt(i);
    318             if (viewType.isAssignableFrom(v.getClass())) list.add(viewType.cast(v));
    319         }
    320         return list;
    321     }
    322 
    323     /**
    324      * Selects the checkboxes for the given list of flags.
    325      *
    326      * @param flags A list of mIntent flags to select.
    327      */
    328     public void selectFlags(List<String> flags) {
    329         getAllCheckBoxes().forEach(box -> {
    330             if (flags.contains(box.getText())) {
    331                 box.setChecked(true);
    332             }
    333         });
    334     }
    335 
    336     /**
    337      * Selects the checkboxes for the given list of flags.
    338      *
    339      * @param flags A list of mIntent flags to select.
    340      */
    341     public void selectFlags(Collection<IntentFlag> flags) {
    342         selectFlags(flags.stream().map(IntentFlag::getName).collect(Collectors.toList()));
    343     }
    344 
    345     private void enableAllFlags() {
    346         getAllCheckBoxes().forEach(box -> box.setEnabled(true));
    347     }
    348 
    349     private Collection<CheckBox> getChecked() {
    350         return getAllCheckBoxes().stream().filter(CompoundButton::isChecked)
    351                 .collect(Collectors.toList());
    352     }
    353 
    354     private Collection<IntentFlag> getCheckedFlags() {
    355         return getChecked().stream().map(checkBox -> (IntentFlag) checkBox.getTag(TAG_FLAG))
    356                 .collect(Collectors.toList());
    357     }
    358 
    359     private void disableFlags(Collection<IntentFlag> flags) {
    360         flags.forEach(flag -> getCheckBox(flag).setEnabled(false));
    361     }
    362 
    363     private CheckBox getCheckBox(IntentFlag flag) {
    364         return getAllCheckBoxes().stream().filter(box -> flag.getName().equals(box.getText()))
    365                 .findFirst().orElse(null);
    366     }
    367 
    368     /**
    369      * A functional interface that represents the action to take upon the user pressing the launch
    370      * button within this view.
    371      */
    372     public interface OnLaunchCallback {
    373         void launchActivity(Intent intent, boolean forResult);
    374     }
    375 }
    376