Merge branch 'refactor-setupwizard' into master
* Revamped setup process!
This commit is contained in:
commit
cc90c170e0
|
@ -0,0 +1,4 @@
|
|||
[submodule "libs/SetupWizardLibrary"]
|
||||
path = libs/SetupWizardLibrary
|
||||
url = https://cgit.typeblog.net/SetupWizardLibrary.git
|
||||
branch = android11-dev
|
|
@ -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>
|
|
@ -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'
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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%" />
|
|
@ -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%" />
|
|
@ -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%" />
|
|
@ -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%" />
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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'
|
Loading…
Reference in New Issue