Masking

Masking Views

There may be parts of the app that you want to exclude from being recorded. The ForeSee framework provides methods to allow you to mask parts of your application on a view by view basis. The most common use case is to override the onResume(Bundle savedInstanceState) method in the Activity and register specific view instances for masking. Example:

@Override
public void onResume(Bundle savedInstanceState) {
    View aView = findViewById(R.id.someView);
    ForeSeeReplay.maskView(aView);
}

Any subclass of android.view.View can be masked.

When views are masked, touches within the view are still captured, but the full area of the view is masked.

In the case of ‘EditText’ widgets, only the text is masked, allowing the viewer to see the progress of the user as they enter text without showing the text itself.

Automatic Masking

All fields of type ‘EditText’, 'Spinner', ‘TimePicker’, and ‘DatePicker’ are masked automatically by default. You can mark certain fields to be unmasked, in a similar way to manually masking views described above.

@Override
public void onResume(Bundle savedInstanceState) {
    EditText textField = findViewById(R.id.someEditViewId);
    ForeSeeReplay.unmaskView(textField);
}

📘

Note

Password fields (android:inputType="textPassword") are always masked, regardless of whether ForeSeeReplay.unmaskView() is called.

Masking Activities

If there is an entire Android activity that you want Replay to ignore, you can add the @RecordDisabled annotation before the class definition to disable recording on that activity:

@RecordDisabled  
public class YourActivity extends Activity {
    // Your class definition
}

WebViews

WebViews present a particular challenge to the capture system and masking can be unreliable during some zoom operations. Therefore, it is important that any WebView which contains masked or auto-masked items must have zooming disabled to avoid revealing personal data in the recording.

To ensure the WebView has zooming disabled, verify it has the following meta tag:

<meta name="viewport" content="initial-scale=1, user-scalable=no"/>

It is not necessary to disable zooming on pages which do not contain masked views.

Like native views, all input fields within an unmasked WebView are automatically masked. To mask extra areas, or unmask views which have been automatically masked, use one of the following methods:

CSS Selectors

This is the recommended way to handle masking and unmasking since it is not necessary to alter the HTML of the web page. Instead, CSS selectors can be used in the app configuration to identify areas, or sets of areas, which require masking or unmasking. For example, [name=your_element_name] selects all elements with the name “your_element_name”.

Selectors can be applied to all web pages displayed within the app, as well as restricted to a certain page or set of pages. To implement CSS selectors, add a file called foresee_masking.json to the project and insert the selectors according to the following format:

{
    "webview_masking" : {
        "unmasked" : {
            "" : "\*[class\*='substringMatchingExample']",
            "domain.com" : "input.country"
        },
        "masked" : {
            "billing" : ".username, .address, .secret",
            "support.domain.com" : "div\#chat"
        }
    }
}

Masks added or removed in this way are only applied to a page if the URL contains the snippet specified in the configuration. You can be as specific or general as you like by including as much or as little of the URL as desired. It is also possible to add selectors to apply to all pages by leaving the URL string empty.

The configuration in the example above ensures that any inputs with the class country are unmasked in any page with the domain “domain.com.” Masks are also added to any elements with the ID chat on pages from the domain “support.domain.com” and all elements with class ‘username’, ‘address’, or ‘secret’ on any page where “billing” appears in the URL.

Default WebView Masks
In addition to the elements defined in the foresee_masking.json file, the following elements are masked by default:

  • of type: 'text', 'password', 'number', 'email', 'url', 'search', 'color', or 'tel'
  • <br>
  • CSS Classes This method requires the code of the web page in question to be modified by applying ForeSee masking classes to the required elements. Since this method is governed by the CSS classes in the web page code, the app code does not need to be changed, making this method useful for changing the masking criteria after an app has been released. CSS classes override any CSS selectors that have been applied. To mask or unmask an element, apply the ‘fs-masked’ or ‘fs-unmasked’ class to the element as follows: <div class="fs-masked"> This div is masked. </div> <!-- This input is unmasked --> <!-- fs-unmasked overrides the default masking behaviour --> <input type=text class="fs-unmasked"/> Masking Listviews Just as other views should be masked in onResume, so should listview children. Without updating the mask set in onResume, list items may be temporarily unmasked until the list is scrolled or rotated. Solutions may vary depending on the list implementation, but one potential implementation is: @Override protected void onResume() { super.onResume(); ForeSee.activityResumed(this); listView.invalidateViews(); // this works, too // adapter.notifyDataSetChanged(); } This causes the visible views to be redrawn and re-masked if needed. If the list adapter is using the view holder pattern, then the views are reused and updated as the user scrolls. In this case, it makes sense to mask/unmask views in each getView() call. Here's a simple example without list view types: @Override public View getView(int index, View view, ViewGroup viewGroup) { if (view == null) { view = LayoutInflater.from(context) .inflate(getResources() .getLayout(R.layout.maskable_list_item), viewGroup, false); Holder holder = new Holder((TextView) view.findViewById(R.id.list_item_title)); view.setTag(holder); } ForeSeeReplay.unmaskView(view); Holder holder = (Holder) view.getTag(); int viewType = index % 3; switch (viewType) { case 0: holder.textView.setText("Mask this view"); ForeSeeReplay.maskView(view); break; case 1: holder.textView.setText("Unmask this view"); break; } return view; } Masking Recyclerviews Recyclerviews should be handled much like a listview with the view holder pattern. OnResume() of the activity/fragment should refresh the view to update masks: protected void onResume() { super.onResume(); ForeSee.activityResumed(this); recyclerAdapter.notifyDataSetChanged(); } In the following implementation, two view types are used. If you do this, you do not need to unmask every view as in the listView example because, when the views are recycled, the proper view type is re-used. class ViewHolder extends RecyclerView.ViewHolder { public ImageView image; public TextView text; public ViewHolder(View itemView) { super(itemView); image = (ImageView) itemView.findViewById(R.id.imageListItemImage); text = (TextView) itemView.findViewById(R.id.imageListItemText); } public void setMasked() { ForeSeeReplay.unmaskView(itemView); } } class MaskedViewHolder extends ViewHolder { public MaskedViewHolder(View itemView) { super(itemView); } public void setMasked() { ForeSeeReplay.maskView(itemView); } } private class RecyclerAdapter extends RecyclerView.Adapter { private static final int MASKED = 1; private List items; public RecyclerAdapter(List items) { this.items = items; } @Override public int getItemViewType(int position) { if (items.get(position).masked) { return MASKED; } else { return super.getItemViewType(position); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.image_list_item, parent, false); if (viewType == MASKED) { return new MaskedViewHolder(v); } return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { ImageListItem imageListItem = items.get(position); holder.text.setText(imageListItem.text); holder.image.setImageDrawable(getResources() .getDrawable(imageListItem.icon)); holder.setMasked(); } Masking Snackbars Snackbars are created much like a Toast in Android, but differ in that they are attached to a provided view. In order to mask a Snackbar, the Snackbar's view can be retrieved and masked before showing the Snackbar: Snackbar snackbar = Snackbar.make(view, "Yummy snackbar", Snackbar.LENGTH_SHORT); ForeSeeReplay.maskView(snackbar.getView()); snackbar.show(); Debugging Your Masking Setup As of version 3.3.1, it is possible to review your masking setup in real time by observing the masks in your app as you use them. To enable this feature, add the following command just before you start the ForeSee® SDK: ForeSeeReplay.setMaskingDebugEnabled(true); This feature highlights all masked areas using a translucent red area. All interactions with your app should continue as normal, but there are a couple of points to bear in mind when using this 'live masking': This is not intended as a tool to debug the timing of masks - only to confirm that the right UI items are being selected as masked and unmasked. Live masking only gives a precise reflection of the final masks that will appear in the replay when the view is static. Although the live masks represent exactly what will be masked, they update more frequently than masks are sent to the final masking system. So, although some items may appear unmasked during live masking, final masking does not contain these skipped masks. Once you have confirmed that the correct items are masked, it is recommended to check the final replays and verify the masks are being applied correctly. Since it produces frequent changes on screen, live masking interferes with the process used to create the final masks and therefore replays. For this reason, replays should only be checked with live masking disabled.