Manage certificates
Last updated February 8th, 2024
Now, you can use the Knox APIs to manage certificates in TIMA CCM.
Create CCM Profile
This method initializes a CCMProfile object, sets its accessControlMethod to default and asks the user if they want to allowlist all the packages in the container.
private void createCCMProfile() {
// Initialize a CCMProfile object
mCCMProfileObj = new CCMProfile();
// Set its accessControlMethod to the default method
mCCMProfileObj.accessControlMethod = CCMProfile.AccessControlMethod.LOCK_STATE;
// Proceed to ask the user if they want to allowlist all packages
whitelistAllPackages();
}
Allowlist all packages
This method asks the user if they want to allowlist all the packages in the container. Allowlisting allows them access to the token. If they do, the app skips asking the user to allowlist specific package names and set the CCM profile. If they don’t, it asks the user to allowlist specific package names before setting the CCM profile.
private void whitelistAllPackages() {
// Create a dialog box to show to the user
new AlertDialog.Builder(this)
// Set the dialog box's title
.setTitle(getString(R.string.whitelist_all_packages))
// Set the dialog box's message
.setMessage(getString(R.string.whitelist_all_packages_message))
// Set the action buttons
.setPositiveButton(getString(R.string.opt_yes), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Allowlist all the packages, then set the CCM profile
mCCMProfileObj.whiteListAllPackages = true;
setCCMProfile();
}
}).setNegativeButton(getString(R.string.opt_no), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Ask the user for specific package names to be allowlisted
mCCMProfileObj.whiteListAllPackages = false;
whitelistPackagesToAccessToken();
}
}).show();
}
Allowlist specific packages
This method prompts the user to select certain packages in the container to be allowlisted, allowing them to access the token.
private void whitelistPackagesToAccessToken() {
// List to contain the user's selected packages
final List<String> selectedPackages = new ArrayList<>();
// List of installed packages on the device
final List<String> installedPackages = getInstalledPackages();
// Create a dialog box to show to the user
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// Set the dialog box's title
builder.setTitle(getString(R.string.whitelist_packages_list_title));
// Convert the list of installed packages to an array
final String[] installedPackagesArr = installedPackages.toArray(
new String[installedPackages.size()]);
builder.setMultiChoiceItems(installedPackagesArr, null,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int indexSelected,
boolean isChecked) {
// If user selects a package in the list
String selectedPackage = installedPackagesArr[indexSelected];
// If the package is checked, then add it to the selected items
if (isChecked) {
selectedPackages.add(selectedPackage);
} else if (selectedPackages.contains(selectedPackage)) {
selectedPackages.remove(selectedPackage);
}
}
})
// Set the action buttons
.setPositiveButton(getString(R.string.option_confirm), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Set the CCM Profile's packageList to that of allowlisted packages by user
mCCMProfileObj.packageList = selectedPackages;
// Proceed to apply the CCM profile
setCCMProfile();
dialog.dismiss();
}
})
.setNegativeButton(getString(R.string.option_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Cancel CCM Profile creation
mCCMProfileObj = null;
mUtils.log(getString(R.string.cancel_ccm_profile));
dialog.dismiss();
}
});
AlertDialog dialog = builder.create();
// Show the dialog box to the user
dialog.show();
}
Set CCM Profile
This method applies the CCM profile created by the user through the dialog boxes.
private void setCCMProfile () {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the setCCMProfile method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
mUtils.log(getString(R.string.about_to_apply_profile));
// display the current CCM Profile Object's access method
mUtils.log(getString(R.string.display_ccm_access, mCCMProfileObj.accessControlMethod));
// display whether the CCM profile allowlisted all packages
mUtils.log(getString(R.string.display_ccm_whitelist_all_packages,
mCCMProfileObj.whiteListAllPackages));
// if not allowlisting all packages, display each allowlisted package
if (!mCCMProfileObj.whiteListAllPackages) {
mUtils.log(getString(R.string.display_ccm_packagelist));
// display the CCM profile's package list
for (String packageName : mCCMProfileObj.packageList) {
mUtils.log(getString(R.string.input_package_name, packageName));
}
}
try {
// Apply the CCM Profile
boolean result = clientCertificateManager.setCCMProfile(mCCMProfileObj);
// Display CCM profile set result
if (result) {
mUtils.log(getString(R.string.ccm_profile_set_successfully));
} else {
mUtils.log(getString(R.string.ccm_profile_failed));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Get CCM Profile
This method gets the current CCM profile set through setCCMProfile, and displays its details to the user.
private void getCCMProfile () {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the setCCMProfile method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
CCMProfile currentProfile = clientCertificateManager.getCCMProfile();
if (currentProfile != null) {
mUtils.log(getString(R.string.display_current_profile));
// display the current CCM Profile Object's access method
mUtils.log(getString(R.string.display_ccm_access,
currentProfile.accessControlMethod));
// display whether the CCM profile allowlisted all packages
mUtils.log(getString(R.string.display_ccm_whitelist_all_packages,
currentProfile.whiteListAllPackages));
// if not allowlisting all packages, display each allowlisted package
if (!currentProfile.whiteListAllPackages) {
mUtils.log(getString(R.string.display_ccm_packagelist));
// display the CCM profile's package list
for (String packageName : currentProfile.packageList) {
mUtils.log(getString(R.string.input_package_name, packageName));
}
}
} else {
mUtils.log(getString(R.string.no_profile_set));
}
} catch (Exception e) {
mUtils.processException(e, TAG);
}
}
Delete CCM Profile
This method deletes the current CCM Profile.
private void deleteCCMProfile () {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the setCCMProfile method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager
.getClientCertificateManagerPolicy();
try {
boolean result = clientCertificateManager.deleteCCMProfile();
if (result) {
// Delete CCM Profile result
mUtils.log(getString(R.string.ccm_profile_delete_successfully));
} else {
mUtils.log(getString(R.string.ccm_profile_delete_failed));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Get default Certificate Alias
This method returns the default certificate alias, if one exists for the device. The default certificate is a device-unique certificate rooted at the Samsung root certificate, and can be used to uniquely identify a Samsung device.
private void getDefaultCertificateAlias() {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the setCCMProfile method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
String defaultAlias = clientCertificateManager.getDefaultCertificateAlias();
if (defaultAlias != null) {
// Display the default certificate alias
mUtils.log(getString(R.string.get_default_certificate, defaultAlias));
} else {
// No default certificate alias is available
mUtils.log(getString(R.string.no_default_certificate));
}
} catch (Exception e) {
mUtils.processException(e, TAG);
}
}
Create Certificate
This method initializes a CertificateProfile object, and asks the user to set an alias for it.
private void createCertificate() {
mCertificateProfile = new CertificateProfile();
mUtils.log(getString(R.string.creating_new_certificate_profile));
configureCertificate();
}
Configure Certificate
This method asks the user to provide a certificate alias, as well as choose to allow or disallow Wi-Fi, allow or disallow all packages, and enable or disable CSRResponse.
private void configureCertificate() {
// Build a dialog to show the user
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// WIth the title "Configure Certificate"
builder.setTitle(getString(R.string.configure_certificate));
View viewInflated = LayoutInflater.from(this).
inflate(R.layout.prompt_certificate_alias_wifi_packages_csrresponse,
(ViewGroup) findViewById(R.id.configureCertificateGroup), false);
final EditText edTxtCertAlias = viewInflated.findViewById(R.id.edtxtCertificateAlias);
final CheckBox chkAllowAllPackages = viewInflated.findViewById(R.id.chkAllowAllPackages);
final CheckBox chkAllowWifi = viewInflated.findViewById(R.id.chkAllowWifi);
final CheckBox chkIsCSRResponse = viewInflated.findViewById(R.id.chkIsCSRResponse);
builder.setView(viewInflated);
builder.setPositiveButton(getString(R.string.option_confirm),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mCertificateProfile.alias = edTxtCertAlias.getText().toString();
mCertificateProfile.allowAllPackages = chkAllowAllPackages.isChecked();
mCertificateProfile.allowWiFi = chkAllowWifi.isChecked();
mCertificateProfile.isCSRResponse = chkIsCSRResponse.isChecked();
mUtils.log(getString(R.string.configuring_certificate_with));
mUtils.log(getString(R.string.certificate_alias_is,
mCertificateProfile.alias));
mUtils.log(getString(R.string.certificate_allow_all_packages_is,
mCertificateProfile.allowAllPackages));
mUtils.log(getString(R.string.certificate_allow_wifi_is,
mCertificateProfile.allowWiFi));
mUtils.log(getString(R.string.certificate_iscsrresponse_is,
mCertificateProfile.isCSRResponse));
// If provided alias is not empty string
if (!(mCertificateProfile.alias).equals("")) {
// Check if allow all packages checked
if (mCertificateProfile.allowAllPackages) {
// If it is, skip choosing specific packages to choose
setCertificatePathAndPassword();
} else {
// If it isn't, prompt user for specific packages to choose
allowPackagesToAccessCertificate();
}
} else {
// If provided alias is empty, prompt user for valid alias
mUtils.log(getString(R.string.provide_valid_alias));
}
}
});
builder.setNegativeButton(getString(R.string.option_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Certificate creation cancelled. Reset the certificate profile.
mCertificateProfile = null;
mUtils.log(getString(R.string.certificate_profile_cancelled));
dialog.cancel();
}
});
builder.show();
}
Allow packages to access current Certificate
private void allowPackagesToAccessCertificate () {
// List to contain the user's selected packages
final List<String> selectedPackages = new ArrayList<>();
// List of installed packages on the device
final List<String> installedPackages = getInstalledPackages();
// Create a dialog box to show to the user
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// Set the dialog box's title
builder.setTitle(getString(R.string.whitelist_packages_list_title));
// Convert the list of installed packages to an array
final String[] installedPackagesArr = installedPackages.toArray(
new String[installedPackages.size()]);
builder.setMultiChoiceItems(installedPackagesArr, null,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int indexSelected,
boolean isChecked) {
// If user selects a package in the list
String selectedPackage = installedPackagesArr[indexSelected];
// If package is checked, then add it to the selected items
if (isChecked) {
selectedPackages.add(selectedPackage);
} else if (selectedPackages.contains(selectedPackage)) {
selectedPackages.remove(selectedPackage);
}
}
})
// Set the action buttons
.setPositiveButton(getString(R.string.option_confirm),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Set the CertificateProfile's packageList to that of allowlisted packages
mCertificateProfile.packageList = selectedPackages;
// Proceed to set CertificateProfile's path and password
setCertificatePathAndPassword();
dialog.dismiss();
}
})
.setNegativeButton(getString(R.string.option_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Certificate creation cancelled. Reset the certificate profile.
mCertificateProfile = null;
mUtils.log(getString(R.string.certificate_profile_cancelled));
dialog.dismiss();
}
});
AlertDialog dialog = builder.create();
// Show the dialog box to the user
dialog.show();
}
Set Certificate path and password
This method sets the current certificate’s path and password.
private void setCertificatePathAndPassword() {
// Build a dialog for the user
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// with the title "Set Certificate Path and Password"
builder.setTitle(getString(R.string.set_cert_path_and_pass));
View viewInflated = LayoutInflater.from(this).
inflate(R.layout.prompt_certificate_path_and_password,
(ViewGroup) findViewById(R.id.certificatePassAndPathGroup),
false);
final EditText edTxtCertPath = viewInflated.findViewById(R.id.edTxtCertificatePath);
final EditText edTxtCertPassword = viewInflated.findViewById(R.id.edTxtCertificatePassword);
builder.setView(viewInflated);
builder.setPositiveButton(getString(R.string.option_confirm),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String path = Environment.getExternalStorageDirectory() + "/";
String password = edTxtCertPassword.getText().toString();
mUtils.log(getString(R.string.provided_cert_path_and_pass));
// Install the path and password
installCertificate(path, password);
}
});
builder.setNegativeButton(getString(R.string.option_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Certificate creation cancelled. Reset the certificate profile.
mCertificateProfile = null;
mUtils.log(getString(R.string.certificate_profile_cancelled));
dialog.cancel();
}
});
builder.show();
}
Install Certificate
This method installs the current certificate with user-provided details.
private void installCertificate(String path, String password) {
mUtils.log(getString(R.string.installing_certificate));
// Initialize the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the installCertificate method lives
ClientCertificateManager clientCertificateManager =
enterpriseKnoxManager.getClientCertificateManagerPolicy();
try {
grantReadExternalStorageRuntimePermission();
// Get the byte array from the provided path
byte[] certificateBuffer = Utils.getByteArrayFromPath(path);
// Install the certificate with the profile config, byte array and password provided
boolean result = clientCertificateManager.
installCertificate(mCertificateProfile, certificateBuffer, password);
if (result) {
mUtils.log(getString(R.string.certificate_profile_installed_successfully));
} else {
mUtils.log(getString(R.string.certificate_profile_installation_failed));
}
} catch (Exception e) {
mUtils.processException(e, TAG);
}
}
Get installed packages
This method returns a list of installed package names on the device.
private List<String> getInstalledPackages () {
// Get list of installed packages
List<PackageInfo> packages = getPackageManager().getInstalledPackages(0);
// Initialize a list of package names
ArrayList<String> packageList = new ArrayList<>();
// For each installed package
for (PackageInfo pInfo : packages) {
// Add its package name to the return list
packageList.add(pInfo.packageName);
}
return packageList;
}
Prompt user to delete Certificate
This method prompts the user for a certificate alias to delete.
private void promptUserToDeleteCertificate () {
// Create a dialog box
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// with the title "Delete Certificate."
builder.setTitle(getString(R.string.delete_certificate));
View viewInflated = LayoutInflater.from(this).
inflate(R.layout.prompt_certificate_alias_to_delete,
(ViewGroup) findViewById(R.id.deleteCertGroup), false);
final EditText edTxtCertAlias = viewInflated.findViewById(R.id.edtxtCertificateAlias);
builder.setView(viewInflated);
builder.setPositiveButton(getString(R.string.option_confirm),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Get the user provided alias
String alias = edTxtCertAlias.getText().toString();
// If alias is valid
if (!alias.equals("")) {
// Delete the certificate with an alias that matches provided alias.
deleteCertificate(alias);
} else {
mUtils.log(getString(R.string.no_alias_entered));
}
}
});
builder.setNegativeButton(getString(R.string.option_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mCertificateProfile = null;
mUtils.log(getString(R.string.certificate_profile_deletion_cancelled));
dialog.cancel();
}
});
builder.show();
}
Delete certificate
This method deletes a certificate with an alias that matches the user-provided alias.
private void deleteCertificate (String alias) {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the setCCMProfile method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
// attempt to delete the certificate
boolean result = clientCertificateManager.deleteCertificate(alias);
if (result) {
mUtils.log(getString(R.string.certificate_profile_deleted_successfully));
} else {
mUtils.log(getString(R.string.certificate_profile_deletion_failed));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Prompt user for a package name
This method prompts the user for a package name to either add or remove from the exempt list, or check if it is affected by CCM policy.
private void promptUserForPackage (final int type) {
// Create a dialog box
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// With a title depending on what button the user pressed
switch (type) {
// Title to add package to exempt list
case ADD_PACKAGE:
builder.setTitle(getString(R.string.add_package_to_exempt_list));
break;
// Title to remove package from exempt list
case REMOVE_PACKAGE:
builder.setTitle(getString(R.string.remove_package_from_exempt_list));
break;
// Title to check if CCM policy is enabled for a package
case CHECK_PACKAGE:
builder.setTitle(getString(R.string.check_ccm_policy_enabled_for_package));
break;
default:
return;
}
View viewInflated = LayoutInflater.from(this).
inflate(R.layout.prompt_package_name,
(ViewGroup) findViewById(R.id.packageNameInputGroup), false);
final EditText edTxtPackageName = viewInflated.findViewById(R.id.edTxtPackageName);
builder.setView(viewInflated);
builder.setPositiveButton(getString(R.string.option_confirm),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Get the user provided package name
String packageName = edTxtPackageName.getText().toString();
switch (type) {
case ADD_PACKAGE:
addPackageToExemptList(packageName);
break;
case REMOVE_PACKAGE:
removePackageFromExemptList(packageName);
break;
case CHECK_PACKAGE:
checkCCMEnabledForPackage(packageName);
break;
default:
break;
}
}
});
builder.setNegativeButton(getString(R.string.option_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.show();
}
Add package to the exempt list
This method adds the given package name to the exempt list. This is required in case an app (running in background) needs access to certificates when the device or container is locked, or the user is not expected to enter a password. An example of a package that requires exemption is a third-party VPN application that needs to access certificates without user interaction.
private void addPackageToExemptList (String packageName) {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the addPackageToExemptList method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
// attempt to add package name to exempt list
boolean result = clientCertificateManager.addPackageToExemptList(packageName);
if (result) {
mUtils.log(getString(R.string.package_name_added_exempt_list_success));
} else {
mUtils.log(getString(R.string.package_name_added_exempt_list_failed));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Remove package from the exempt list
This method removes the given package name from the exempt list.
private void removePackageFromExemptList (String packageName) {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the removePackageFromExemptList method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
// attempt to remove package name from exempt list
boolean result = clientCertificateManager.removePackageFromExemptList(packageName);
if (result) {
mUtils.log(getString(R.string.package_name_remove_exempt_list_success));
} else {
mUtils.log(getString(R.string.package_name_remove_exempt_list_failed));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Check CCM Policy for a package
This method checks if a CCM policy is enabled for a package.
private void checkCCMEnabledForPackage (String packageName) {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the ClientCertificateManager where the isCCMPolicyEnabledForPackage method lives
ClientCertificateManager clientCertificateManager = enterpriseKnoxManager.
getClientCertificateManagerPolicy();
try {
// check if a CCM policy is enabled for the package
boolean result = clientCertificateManager.isCCMPolicyEnabledForPackage(packageName);
if (result) {
mUtils.log(getString(R.string.ccm_policy_enabled_for_package, packageName));
} else {
mUtils.log(getString(R.string.ccm_policy_not_enabled_for_package, packageName));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Toggle TIMA Keystore state
This method enables/disables the TIMA Keystore state.
private void toggleTimaKeystoreState () {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the TimaKeystore object where the enableTimaKeystore method lives
TimaKeystore timaKeystore = enterpriseKnoxManager.getTimaKeystorePolicy();
try {
// Check current status of the TimaKeystore
boolean isTimaKeystoreEnabled = timaKeystore.isTimaKeystoreEnabled();
mUtils.log("Attempting to toggle. Is enabled? " + isTimaKeystoreEnabled);
// Enable/disable the Tima Keystore
boolean result = timaKeystore.enableTimaKeystore(!isTimaKeystoreEnabled);
if (result) {
mUtils.log(getString(R.string.toggled_tima_keystore, !isTimaKeystoreEnabled));
} else {
mUtils.log(getString(R.string.failed_to_toggle_tima_keystore));
}
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Display TIMA Keystore status
This method displays the TIMA Keystore status as “enabled” or “disabled”.
private void isTimaKeystoreEnabled () {
// Instantiate the EnterpriseKnoxManager class
EnterpriseKnoxManager enterpriseKnoxManager = EnterpriseKnoxManager.getInstance(this);
// Get the TimaKeystore object where the enableTimaKeystore method lives
TimaKeystore timaKeystore = enterpriseKnoxManager.getTimaKeystorePolicy();
try {
// Check current status of the TimaKeystore
boolean isTimaKeystoreEnabled = timaKeystore.isTimaKeystoreEnabled();
mUtils.log(getString(R.string.tima_keystore_status, isTimaKeystoreEnabled));
} catch (SecurityException e) {
mUtils.processException(e, TAG);
}
}
Tutorial progress
You’ve completed 6/7 steps!
NextOn this page
Is this page helpful?