Merge branch 'refactor-setupwizard' into master

* Revamped setup process!
This commit is contained in:
Peter Cai 2021-03-17 16:38:43 +08:00
commit cc90c170e0
19 changed files with 544 additions and 93 deletions

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "libs/SetupWizardLibrary"]
path = libs/SetupWizardLibrary
url = https://cgit.typeblog.net/SetupWizardLibrary.git
branch = android11-dev

View File

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libs/SetupWizardLibrary" vcs="Git" />
</component>
</project>

View File

@ -43,6 +43,8 @@ dependencies {
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
debugImplementation project(path: ':setup-wizard-lib', configuration: 'gingerbreadCompatDebugRuntimeElements')
releaseImplementation project(path: ':setup-wizard-lib', configuration: 'gingerbreadCompatReleaseRuntimeElements')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.4.0-alpha04'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'

View File

@ -43,6 +43,11 @@
</intent-filter>
</activity>
<!-- Setup Wizard -->
<activity android:name=".ui.SetupWizardActivity"
android:theme="@style/SuwThemeMaterial.Light"
android:launchMode="singleTask" />
<!-- The Settings activity -->
<activity android:name=".ui.SettingsActivity"
android:label="@string/settings" />

View File

@ -238,8 +238,8 @@ public class DummyActivity extends Activity {
.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
LocalStorageManager.getInstance()
.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, false);
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(new ComponentName(this, MainActivity.class));
Intent intent = new Intent(SetupWizardActivity.ACTION_PROFILE_PROVISIONED);
intent.setComponent(new ComponentName(this, SetupWizardActivity.class));
startActivity(intent);
Toast.makeText(this, getString(R.string.provision_finished), Toast.LENGTH_LONG).show();
finish();

View File

@ -1,6 +1,5 @@
package net.typeblog.shelter.ui;
import android.app.ProgressDialog;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Intent;
@ -15,6 +14,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@ -30,12 +30,10 @@ import com.google.android.material.tabs.TabLayoutMediator;
import net.typeblog.shelter.R;
import net.typeblog.shelter.ShelterApplication;
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
import net.typeblog.shelter.services.IAppInstallCallback;
import net.typeblog.shelter.services.IShelterService;
import net.typeblog.shelter.services.IStartActivityProxy;
import net.typeblog.shelter.services.KillerService;
import net.typeblog.shelter.util.AuthenticationUtility;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.SettingsManager;
import net.typeblog.shelter.util.UriForwardProxy;
@ -45,17 +43,18 @@ public class MainActivity extends AppCompatActivity {
public static final String BROADCAST_CONTEXT_MENU_CLOSED = "net.typeblog.shelter.broadcast.CONTEXT_MENU_CLOSED";
public static final String BROADCAST_SEARCH_FILTER_CHANGED = "net.typeblog.shelter.broadcast.SEARCH_FILTER_CHANGED";
private static final int REQUEST_PROVISION_PROFILE = 1;
private static final int REQUEST_START_SERVICE_IN_WORK_PROFILE = 2;
private static final int REQUEST_TRY_START_SERVICE_IN_WORK_PROFILE = 4;
private static final int REQUEST_DOCUMENTS_CHOOSE_APK = 5;
private final ActivityResultLauncher<Void> mStartSetup =
registerForActivityResult(new SetupWizardActivity.SetupWizardContract(), this::setupWizardCb);
private final ActivityResultLauncher<Void> mResumeSetup =
registerForActivityResult(new SetupWizardActivity.ResumeSetupContract(), this::setupWizardCb);
private LocalStorageManager mStorage = null;
private DevicePolicyManager mPolicyManager = null;
// The "please wait" dialog when creating profile
private ProgressDialog mProgressDialog = null;
// Flag to avoid double-killing our services while restarting
private boolean mRestarting = false;
@ -86,32 +85,13 @@ public class MainActivity extends AppCompatActivity {
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (mProgressDialog != null && isWorkProfileAvailable()) {
mProgressDialog.dismiss();
init();
}
}
private void init() {
if (mStorage.getBoolean(LocalStorageManager.PREF_IS_SETTING_UP) && !isWorkProfileAvailable()) {
// Provision is still going on...
Toast.makeText(this, R.string.provision_still_pending, Toast.LENGTH_SHORT).show();
finish();
if (mStorage.getBoolean(LocalStorageManager.PREF_IS_SETTING_UP) && !Utility.isWorkProfileAvailable(this)) {
// System has already finished provisioning, but Shelter still
// needs to be brought up inside the work profile
mResumeSetup.launch(null);
} else if (!mStorage.getBoolean(LocalStorageManager.PREF_HAS_SETUP)) {
// Reset the authentication key first
AuthenticationUtility.reset();
// If not set up yet, we have to provision the profile first
new AlertDialog.Builder(this)
.setCancelable(false)
.setMessage(R.string.first_run_alert)
.setPositiveButton(R.string.first_run_alert_continue,
(dialog, which) -> setupProfile())
.setNegativeButton(R.string.first_run_alert_cancel,
(dialog, which) -> finish())
.show();
mStartSetup.launch(null);
} else {
// Initialize the settings
SettingsManager.getInstance().applyAll();
@ -120,23 +100,11 @@ public class MainActivity extends AppCompatActivity {
}
}
private void setupProfile() {
// Build the provisioning intent first
ComponentName admin = new ComponentName(getApplicationContext(), ShelterDeviceAdminReceiver.class);
Intent intent = new Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, true);
intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, admin);
// Check if provisioning is allowed
if (!mPolicyManager.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
|| getPackageManager().resolveActivity(intent, 0) == null) {
Toast.makeText(this,
getString(R.string.msg_device_unsupported), Toast.LENGTH_LONG).show();
private void setupWizardCb(Boolean result) {
if (result)
init();
else
finish();
}
// Start provisioning
startActivityForResult(intent, REQUEST_PROVISION_PROFILE);
}
private void bindServices() {
@ -233,26 +201,6 @@ public class MainActivity extends AppCompatActivity {
tab.setText(pageTitles[position])).attach();
}
private boolean isWorkProfileAvailable() {
// Determine if the work profile is already available
// If so, return true and set all the corresponding flags to true
// This is for scenarios where the asynchronous part of the
// setup process might be finished before the synchronous part
Intent intent = new Intent(DummyActivity.TRY_START_SERVICE);
try {
// DO NOT sign this request, because this won't be actually sent to work profile
// If this is signed, and is the first request to be signed,
// then the other side would never receive the auth_key
Utility.transferIntentToProfileUnsigned(this, intent);
mStorage.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, false);
mStorage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
return true;
} catch (IllegalStateException e) {
// If any exception is thrown, this means that the profile is not available
return false;
}
}
// Get the service on the other side
// remote (work) -> main
// main -> remote (work)
@ -453,30 +401,7 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_PROVISION_PROFILE) {
if (resultCode == RESULT_OK) {
if (isWorkProfileAvailable()) {
// For pre-Oreo, or post-Oreo on some circumstances,
// by the time this is received, the whole process
// should have completed.
recreate();
return;
}
// The sync part of the setup process is completed
// Wait for the provisioning to complete
mStorage.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, true);
// However, we still have to wait for DummyActivity in work profile to finish
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(getString(R.string.provision_still_pending));
mProgressDialog.setCancelable(false);
mProgressDialog.show();
} else {
Toast.makeText(this,
getString(R.string.work_profile_provision_failed), Toast.LENGTH_LONG).show();
finish();
}
} else if (requestCode == REQUEST_TRY_START_SERVICE_IN_WORK_PROFILE) {
if (requestCode == REQUEST_TRY_START_SERVICE_IN_WORK_PROFILE) {
if (resultCode == RESULT_OK) {
// RESULT_OK is from DummyActivity. The work profile is enabled!
bindWorkService();

View File

@ -0,0 +1,420 @@
package net.typeblog.shelter.ui;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import android.app.admin.DevicePolicyManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.setupwizardlib.SetupWizardLayout;
import com.android.setupwizardlib.view.NavigationBar;
import net.typeblog.shelter.R;
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
import net.typeblog.shelter.util.AuthenticationUtility;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
public class SetupWizardActivity extends AppCompatActivity {
// RESUME_SETUP should be used when MainActivity detects the provisioning has been
// finished by the system, but the Shelter inside the profile has never been brought up
// due to the user having not clicked on the notification yet.
public static final String ACTION_RESUME_SETUP = "net.typeblog.shelter.RESUME_SETUP";
public static final String ACTION_PROFILE_PROVISIONED = "net.typeblog.shelter.PROFILE_PROVISIONED";
private DevicePolicyManager mPolicyManager = null;
private LocalStorageManager mStorage = null;
private final ActivityResultLauncher<Void> mProvisionProfile =
registerForActivityResult(new ProfileProvisionContract(), this::setupProfileCb);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// The user could click on the "finish provisioning" notification while having removed
// this activity from the recents stack, in which case the notification will start a new
// instance of activity
if (ACTION_PROFILE_PROVISIONED.equals(getIntent().getAction()) && Utility.isWorkProfileAvailable(this)) {
// ...in which case we should finish immediately and go back to MainActivity
startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
setContentView(R.layout.activity_setup_wizard);
mPolicyManager = getSystemService(DevicePolicyManager.class);
mStorage = LocalStorageManager.getInstance();
// Don't use switchToFragment for the first time
// because we don't want animation for the first fragment
// (it would have nothing to animate upon, resulting in a black background)
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.setup_wizard_container,
ACTION_RESUME_SETUP.equals(getIntent().getAction()) ?
new ActionRequiredFragment() : new WelcomeFragment())
.commit();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// DummyActivity will start this activity with an empty intent
// once the provision is finalized
if (ACTION_PROFILE_PROVISIONED.equals(intent.getAction()) && Utility.isWorkProfileAvailable(this))
finishWithResult(true);
}
private<T extends BaseWizardFragment> void switchToFragment(T fragment, boolean reverseAnimation) {
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(
reverseAnimation ? R.anim.slide_in_from_left : R.anim.slide_in_from_right,
reverseAnimation ? R.anim.slide_out_to_right : R.anim.slide_out_to_left
)
.replace(R.id.setup_wizard_container, fragment)
.commit();
}
private void finishWithResult(boolean succeeded) {
setResult(succeeded ? RESULT_OK : RESULT_CANCELED);
finish();
}
private void setupProfile() {
if (!mPolicyManager.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)) {
switchToFragment(new FailedFragment(), false);
return;
}
// The user may have aborted provisioning before without clearing data
// This can cause issues if the authentication utility thinks we
// could do authentication due to the presence of keys
AuthenticationUtility.reset();
try {
mProvisionProfile.launch(null);
} catch (ActivityNotFoundException e) {
// How could this fail???
switchToFragment(new FailedFragment(), false);
}
}
private void setupProfileCb(Boolean result) {
if (result) {
if (Utility.isWorkProfileAvailable(this)) {
// On pre-Oreo, and sometimes on post-Oreo
// the setup could be already finalized at this point
// There is no need for more action
finishWithResult(true);
return;
}
// Provisioning finished, but we still need to tell the user
// to click on the notification to bring up Shelter inside the
// profile. Otherwise, the setup will not be complete
mStorage.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, true);
switchToFragment(new ActionRequiredFragment(), false);
} else {
switchToFragment(new FailedFragment(), false);
}
}
public static class SetupWizardContract extends ActivityResultContract<Void, Boolean> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Void input) {
return new Intent(context, SetupWizardActivity.class);
}
@Override
public Boolean parseResult(int resultCode, @Nullable Intent intent) {
return resultCode == RESULT_OK;
}
}
public static class ResumeSetupContract extends ActivityResultContract<Void, Boolean> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Void input) {
Intent intent = new Intent(context, SetupWizardActivity.class);
intent.setAction(ACTION_RESUME_SETUP);
return intent;
}
@Override
public Boolean parseResult(int resultCode, @Nullable Intent intent) {
return resultCode == RESULT_OK;
}
}
private static class ProfileProvisionContract extends ActivityResultContract<Void, Boolean> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Void input) {
ComponentName admin = new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class);
Intent intent = new Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, true);
intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, admin);
return intent;
}
@Override
public Boolean parseResult(int resultCode, @Nullable Intent intent) {
return resultCode == RESULT_OK;
}
}
// ==== SetupWizard steps ====
private static abstract class BaseWizardFragment extends Fragment implements NavigationBar.NavigationBarListener {
protected SetupWizardActivity mActivity = null;
protected SetupWizardLayout mWizard = null;
protected abstract int getLayoutResource();
@Override
public void onNavigateBack() {
// For sub-classes to implement
}
@Override
public void onNavigateNext() {
// For sub-classes to implement
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mActivity = (SetupWizardActivity) getActivity();
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResource(), container, false);
mWizard = view.findViewById(R.id.wizard);
mWizard.getNavigationBar().setNavigationBarListener(this);
mWizard.setLayoutBackground(ContextCompat.getDrawable(inflater.getContext(), R.color.colorAccent));
return view;
}
}
protected static abstract class TextWizardFragment extends BaseWizardFragment {
protected abstract int getTextRes();
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView tv = view.findViewById(R.id.setup_wizard_generic_text);
tv.setText(getTextRes());
}
}
public static class WelcomeFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_welcome_text;
}
@Override
public void onNavigateNext() {
super.onNavigateNext();
mActivity.switchToFragment(new PermissionsFragment(), false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_welcome);
mWizard.getNavigationBar().getBackButton().setVisibility(View.GONE);
}
}
public static class PermissionsFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_permissions_text;
}
@Override
public void onNavigateBack() {
super.onNavigateBack();
mActivity.switchToFragment(new WelcomeFragment(), true);
}
@Override
public void onNavigateNext() {
super.onNavigateNext();
mActivity.switchToFragment(new CompatibilityFragment(), false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_permissions);
}
}
public static class CompatibilityFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_compatibility_text;
}
@Override
public void onNavigateBack() {
super.onNavigateBack();
mActivity.switchToFragment(new PermissionsFragment(), true);
}
@Override
public void onNavigateNext() {
super.onNavigateNext();
mActivity.switchToFragment(new ReadyFragment(), false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_compatibility);
}
}
public static class ReadyFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_ready_text;
}
@Override
public void onNavigateBack() {
super.onNavigateBack();
mActivity.switchToFragment(new CompatibilityFragment(), true);
}
@Override
public void onNavigateNext() {
super.onNavigateNext();
mActivity.switchToFragment(new PleaseWaitFragment(), false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_ready);
}
}
public static class PleaseWaitFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_please_wait_text;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mActivity.setupProfile();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_please_wait);
mWizard.setProgressBarColor(view.getContext().getColorStateList(R.color.setup_wizard_progress_bar));
mWizard.setProgressBarShown(true);
mWizard.getNavigationBar().getBackButton().setVisibility(View.GONE);
mWizard.getNavigationBar().getNextButton().setVisibility(View.GONE);
}
}
public static class ActionRequiredFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_action_required_text;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_action_required);
mWizard.setProgressBarColor(view.getContext().getColorStateList(R.color.setup_wizard_progress_bar));
mWizard.setProgressBarShown(true);
mWizard.getNavigationBar().getBackButton().setVisibility(View.GONE);
mWizard.getNavigationBar().getNextButton().setVisibility(View.GONE);
}
}
public static class FailedFragment extends TextWizardFragment {
@Override
protected int getLayoutResource() {
return R.layout.fragment_setup_wizard_generic_text;
}
@Override
protected int getTextRes() {
return R.string.setup_wizard_failed_text;
}
@Override
public void onNavigateNext() {
super.onNavigateNext();
mActivity.finishWithResult(false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWizard.setHeaderText(R.string.setup_wizard_failed);
mWizard.getNavigationBar().getBackButton().setVisibility(View.GONE);
}
}
}

View File

@ -93,6 +93,27 @@ public class Utility {
}
}
// Determine if the work profile is already available
// If so, return true and set all the corresponding flags to true
// This is for scenarios where the asynchronous part of the
// setup process might be finished before the synchronous part
public static boolean isWorkProfileAvailable(Context context) {
LocalStorageManager storage = LocalStorageManager.getInstance();
Intent intent = new Intent(DummyActivity.TRY_START_SERVICE);
try {
// DO NOT sign this request, because this won't be actually sent to work profile
// If this is signed, and is the first request to be signed,
// then the other side would never receive the auth_key
Utility.transferIntentToProfileUnsigned(context, intent);
storage.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, false);
storage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
return true;
} catch (IllegalStateException e) {
// If any exception is thrown, this means that the profile is not available
return false;
}
}
// Enforce policies and configurations in the work profile
public static void enforceWorkProfilePolicies(Context context) {
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="-100%"
android:toXDelta="0%" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="100%"
android:toXDelta="0%" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0%"
android:toXDelta="-100%" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0%"
android:toXDelta="100%" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true"
android:color="@color/colorAccentSetupWizard" />
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/setup_wizard_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.SetupWizardActivity" />

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.android.setupwizardlib.SetupWizardLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/wizard"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/setup_wizard_generic_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp" />
</com.android.setupwizardlib.SetupWizardLayout>

View File

@ -3,6 +3,7 @@
<color name="colorPrimary">#FAFAFA</color>
<color name="colorPrimaryDark">#C2C2C2</color>
<color name="colorAccent">#009688</color>
<color name="colorAccentSetupWizard">#FFC107</color>
<color name="black">#333333</color>
<color name="grey">#999999</color>
<color name="disabledAppBackground">#E0F2F1</color>

View File

@ -8,6 +8,22 @@
<string name="device_admin_explanation">Shelter needs to become Device Admin in order to perform its isolation tasks.</string>
<string name="camera_proxy_activity">Choose an Image File</string>
<!-- Setup Wizard -->
<string name="setup_wizard_welcome">Welcome to Shelter</string>
<string name="setup_wizard_welcome_text">Shelter is an application to help you run other applications in an isolated profile. It does so by making use of the <b>Work Profile</b> feature of Android.\n\nClick \"Next\", and we will provide you with more information about Shelter, and guide you through the setup process.\n\nWe suggest that you read through all of the following pages carefully.</string>
<string name="setup_wizard_permissions">A word on permissions</string>
<string name="setup_wizard_permissions_text">By default, Shelter will not ask for any individual permissions. However, once you proceed with the setup process, Shelter will try to set up a Work Profile and hence become the <b>profile manager</b> of said profile.\n\nThis will grant Shelter an extensive list of permissions inside the profile, comparable to that of a Device Admin, albeit confined to the profile. Being the profile manager is necessary for most of Shelter\'s functionality.\n\nSome advanced features of Shelter may require more permissions <b>outside</b> the Work Profile. When needed, Shelter will ask for those permissions separately when you enable the corresponding features.</string>
<string name="setup_wizard_compatibility">Compatibility</string>
<string name="setup_wizard_compatibility_text">Shelter is developed and tested on AOSP-like Android derivatives. This includes AOSP (Android Open Source Project), Google Android (on Pixels), and <b>most AOSP-based open-source custom ROMs</b> such as LineageOS. If your phone is running one of the Android derivatives listed above, then congratulations! Shelter is probably going to work correctly on your device.\n\nSome device vendors introduce very invasive customizations into the Android code base, resulting in conflicts, incompatibility and unexpected behavior. Some custom ROMs can also introduce compatibility-breaking changes, but generally these are rarer occurrences compared to phone vendor-introduced incompatibilities.\n\nSheler is merely an interface into the Work Profile feature provided by the system. If the feature provided by the system is broken or non-standard, <b>Shelter could not magically resolve the issue on its own</b>. If you are currently using a vendor-modified Android version that is known to break Work Profiles, <b>you have been warned</b>. You may proceed anyway, but there is no guarantee that Shelter would behave correctly under these circumstances.</string>
<string name="setup_wizard_ready">Ready?</string>
<string name="setup_wizard_ready_text">We are now ready to set up Shelter for you. Please first ensure that your device is <b>not</b> in Do Not Disturb mode, because you will need to <b>click on a notification</b> later to finalize the setup process.\n\nWhen you are ready, click on \"Next\" to begin the setup process.</string>
<string name="setup_wizard_please_wait">Please wait…</string>
<string name="setup_wizard_please_wait_text">We are trying to initialize Work Profile and set up Shelter on your device.</string>
<string name="setup_wizard_failed">Setup failed</string>
<string name="setup_wizard_failed_text">We regret to inform you that we were not able to set up Shelter for you.\n\nIf you did not cancel the setup manually, then the reason for the failure is most commonly due to a heavily modified system, or a conflict between Shelter and other Work Profile managers. Unfortunately, there is not much that we could do about this.\n\nClick "Next" to exit.</string>
<string name="setup_wizard_action_required">Action required</string>
<string name="setup_wizard_action_required_text">You should now be seeing a notification from Shelter. <b>Please click on that notification</b> to finish the setup process.\n\nIf you do not see the notification, make sure your device is not in Do Not Disturb mode and try pulling down the notification center.\n\nTo reset Shelter and start over, you can clear the data of Shelter in Settings.</string>
<!-- Notifications -->
<string name="notifications_important">Shelter Important</string>
<string name="finish_provision_title">Click here to finish setting up Shelter</string>

@ -0,0 +1 @@
Subproject commit 389cc9256395bbe3da587cd2ec7ffc5bf8439487

View File

@ -1 +1,5 @@
include ':app'
include ':setup-wizard-lib'
project(':setup-wizard-lib').projectDir = new File('./libs/SetupWizardLibrary/library')
project(':setup-wizard-lib').buildFileName = 'standalone.gradle'