Replay - Masking

Masking Views

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

@Override
public void onResume(Bundle savedInstanceState) {
    View aView = findViewById(R.id.someView);
    ForeSee.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. The client 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);
    ForeSee.unmaskView(textField);
}

Masking Activities

If there is an entire Android activity that the client would like the recorder to ignore, they 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. It is therefore important that any WebView which contains masked or auto-masked items must have zooming disabled to avoid revealing personal data in the replay.

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 webpages 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 its URL contains the URL snippet specified in the configuration. Clients can be as specific or general as they like by including as much or as little of the URL as they desire. 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:

  • <input> of type: ‘text’, ‘password’, ‘number’, ’email’, ‘url’, ‘search’, ‘color’, or ‘tel’
  • <textarea>
  • <select>

CSS Classes

This method requires the code of the webpage in question to be modified to apply ForeSee masking classes to the elements requiring masking and unmasking. Since this method is governed by the CSS classes in the webpage 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 will be masked
</div>

<!-- This input will be 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 will work too 
    // adapter.notifyDataSetChanged();
}

This will cause the visible views to be redrawn and re-masked if needed.

If the list adapter is using the view holder pattern, then the views will be 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);
    }
    
    ForeSee.unmaskView(view);

    Holder holder = (Holder) view.getTag();
    int viewType = index % 3;
    switch (viewType) {
        case 0:
            holder.textView.setText("Mask this view");
            ForeSee.maskView(view);
            break;
        case 1:
            holder.textView.setText("Unmask this view");
            break;
        }

        return view;
}

Masking Recyclerviews

Recycler views should be handled much like a listview with the view holder pattern.

OnResume() of the activity/fragment should refresh the view to update masks:


@Override
protected void onResume() {
    super.onResume();
    ForeSee.activityResumed(this);
    recyclerAdapter.notifyDataSetChanged();
}

In this implementation, notice we’re using two view types. If we do this, we do not need to unmask every view as we did the listView example. This is because when the views are recycled, the proper view type will be 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() {
        ForeSee.unmaskView(itemView);
    }
}

class MaskedViewHolder extends ViewHolder {

    public MaskedViewHolder(View itemView) {
        super(itemView);
    }

    public void setMasked() {
        ForeSee.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);
Foresee.maskView(snackbar.getView());
snackbar.show();

Debugging Your Masking Setup

From version 3.3.1 onwards, it is possible to review your masking setup in real time by observing the masks in your app as you use it. To enable this feature, add the following command just before you start the ForeSee SDK:

ForeSee.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 will only give 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 will update more frequently than masks are sent to the final masking system, so although some items may appear unmasked during live masking, final masking will not contain these skipped masks.
  • Once you have confirmed that the correct items are masked, it is a good idea to check the final replays to make sure masks are being applied correctly.
  • Since it produces frequent changes on screen, live masking will interfere with the process used to create the final masks and therefore replays. For this reason, replays should only be checked with live masking disabled.

Other articles in this section:

  1. Replay Overview
  2. Sessions
  3. Paging
  4. Masking (current article)
  5. Performance
  6. Limitations
  7. Custom Touch Capture
  8. Custom Image Capture
  9. Performance Techniques
  10. Remote Disable