implement app cloning between profiles
and fixed device policy enforcement issues -- we should set the policies from DummyActivity instead of in the receiver, or it won't work.
This commit is contained in:
parent
40c4670dfe
commit
0a584c1601
|
@ -29,6 +29,7 @@ dependencies {
|
|||
implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||
implementation 'com.android.support:design:28.0.0-rc01'
|
||||
implementation 'com.android.support:localbroadcastmanager:28.0.0-rc01'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
<uses-feature android:name="android.software.managed_users" android:required="true"/>
|
||||
<!--<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>-->
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name=".ShelterApplication"
|
||||
android:allowBackup="false"
|
||||
|
@ -17,9 +20,10 @@
|
|||
|
||||
<!-- The main activity for UI -->
|
||||
<activity android:name=".ui.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="net.typeblog.shelter.action.PROVISION_FINISHED" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
@ -29,8 +33,10 @@
|
|||
<activity android:name=".ui.DummyActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
<intent-filter>
|
||||
<action android:name="net.typeblog.shelter.action.FINALIZE_PROVISION" />
|
||||
<action android:name="net.typeblog.shelter.action.START_SERVICE" />
|
||||
<action android:name="net.typeblog.shelter.action.TRY_START_SERVICE" />
|
||||
<action android:name="net.typeblog.shelter.action.INSTALL_PACKAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// IAppInstallCallback.aidl
|
||||
package net.typeblog.shelter.services;
|
||||
|
||||
interface IAppInstallCallback {
|
||||
void callback(int result);
|
||||
}
|
|
@ -3,11 +3,14 @@ package net.typeblog.shelter.services;
|
|||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
|
||||
import net.typeblog.shelter.services.IAppInstallCallback;
|
||||
import net.typeblog.shelter.services.IGetAppsCallback;
|
||||
import net.typeblog.shelter.services.ILoadIconCallback;
|
||||
import net.typeblog.shelter.util.ApplicationInfoWrapper;
|
||||
|
||||
interface IShelterService {
|
||||
void stopShelterService(boolean kill);
|
||||
void getApps(IGetAppsCallback callback);
|
||||
void loadIcon(in ApplicationInfo info, ILoadIconCallback callback);
|
||||
void installApp(in ApplicationInfoWrapper app, IAppInstallCallback callback);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package net.typeblog.shelter.receivers;
|
||||
|
||||
import android.app.admin.DeviceAdminReceiver;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import net.typeblog.shelter.ui.DummyActivity;
|
||||
import net.typeblog.shelter.util.LocalStorageManager;
|
||||
import net.typeblog.shelter.util.Utility;
|
||||
|
||||
public class ShelterDeviceAdminReceiver extends DeviceAdminReceiver {
|
||||
@Override
|
||||
|
@ -25,12 +23,11 @@ public class ShelterDeviceAdminReceiver extends DeviceAdminReceiver {
|
|||
@Override
|
||||
public void onProfileProvisioningComplete(Context context, Intent intent) {
|
||||
super.onProfileProvisioningComplete(context, intent);
|
||||
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);
|
||||
ComponentName adminComponent = new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class);
|
||||
|
||||
// Enable the profile
|
||||
manager.setProfileEnabled(adminComponent);
|
||||
|
||||
Utility.enforceWorkProfilePolicies(context);
|
||||
// I don't know why setting the policies in this receiver won't work very well
|
||||
// Anyway, we delegate it to the DummyActivity
|
||||
Intent i = new Intent(context.getApplicationContext(), DummyActivity.class);
|
||||
i.setAction(DummyActivity.FINALIZE_PROVISION);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
package net.typeblog.shelter.services;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import net.typeblog.shelter.R;
|
||||
import net.typeblog.shelter.ShelterApplication;
|
||||
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
|
||||
import net.typeblog.shelter.ui.DummyActivity;
|
||||
import net.typeblog.shelter.util.ApplicationInfoWrapper;
|
||||
import net.typeblog.shelter.util.Utility;
|
||||
|
||||
|
@ -22,6 +27,8 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
public class ShelterService extends Service {
|
||||
public static final int RESULT_CANNOT_INSTALL_SYSTEM_APP = 100001;
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "ShelterService";
|
||||
private DevicePolicyManager mPolicyManager = null;
|
||||
private boolean mIsWorkProfile = false;
|
||||
|
@ -77,6 +84,37 @@ public class ShelterService extends Service {
|
|||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installApp(ApplicationInfoWrapper app, IAppInstallCallback callback) throws RemoteException {
|
||||
if ((app.mInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
|
||||
// Installing a non-system app requires firing up PackageInstaller
|
||||
// Delegate this operation to DummyActivity because
|
||||
// Only it can receive a result
|
||||
Intent intent = new Intent(DummyActivity.INSTALL_PACKAGE);
|
||||
intent.setComponent(new ComponentName(ShelterService.this, DummyActivity.class));
|
||||
intent.putExtra("package", app.mInfo.packageName);
|
||||
intent.putExtra("apk", app.mInfo.sourceDir);
|
||||
|
||||
// Send the callback to the DummyActivity
|
||||
Bundle callbackExtra = new Bundle();
|
||||
callbackExtra.putBinder("callback", callback.asBinder());
|
||||
intent.putExtra("callback", callbackExtra);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
if (mIsWorkProfile) {
|
||||
// We can only enable system apps in our own profile
|
||||
mPolicyManager.enableSystemApp(
|
||||
new ComponentName(getApplicationContext(), ShelterDeviceAdminReceiver.class),
|
||||
app.mInfo.packageName);
|
||||
|
||||
callback.callback(Activity.RESULT_OK);
|
||||
} else {
|
||||
callback.callback(RESULT_CANNOT_INSTALL_SYSTEM_APP);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.support.annotation.NonNull;
|
|||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
@ -37,6 +38,18 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
|
|||
mIcon = view.findViewById(R.id.list_app_icon);
|
||||
mTitle = view.findViewById(R.id.list_app_title);
|
||||
mPackage = view.findViewById(R.id.list_app_package);
|
||||
view.setOnClickListener((v) -> onClick());
|
||||
}
|
||||
|
||||
void onClick() {
|
||||
if (mIndex == -1) return;
|
||||
|
||||
// Show available operations via the Fragment
|
||||
// pass the full info to it, since we can't be sure
|
||||
// the index won't change
|
||||
if (mContextMenuHandler != null) {
|
||||
mContextMenuHandler.showContextMenu(mList.get(mIndex), mView);
|
||||
}
|
||||
}
|
||||
|
||||
void setIndex(final int index) {
|
||||
|
@ -83,11 +96,17 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
|
|||
}
|
||||
}
|
||||
|
||||
interface ContextMenuHandler {
|
||||
void showContextMenu(ApplicationInfoWrapper info, View view);
|
||||
}
|
||||
|
||||
private List<ApplicationInfoWrapper> mList = new ArrayList<>();
|
||||
private IShelterService mService;
|
||||
private Drawable mDefaultIcon;
|
||||
private String mLabelDisabled;
|
||||
private boolean mRefreshing = false;
|
||||
private Map<String, Bitmap> mIconCache = new HashMap<>();
|
||||
private ContextMenuHandler mContextMenuHandler = null;
|
||||
private Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private SwipeRefreshLayout mSwipeRefresh;
|
||||
|
@ -98,7 +117,13 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
|
|||
mSwipeRefresh = swipeRefresh;
|
||||
}
|
||||
|
||||
void setContextMenuHandler(ContextMenuHandler handler) {
|
||||
mContextMenuHandler = handler;
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (mRefreshing) return;
|
||||
mRefreshing = true;
|
||||
mSwipeRefresh.setRefreshing(true);
|
||||
mList.clear();
|
||||
mIconCache.clear();
|
||||
|
@ -111,6 +136,7 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
|
|||
mHandler.post(() -> {
|
||||
mSwipeRefresh.setRefreshing(false);
|
||||
notifyDataSetChanged();
|
||||
mRefreshing = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,31 +1,59 @@
|
|||
package net.typeblog.shelter.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import net.typeblog.shelter.R;
|
||||
import net.typeblog.shelter.services.IAppInstallCallback;
|
||||
import net.typeblog.shelter.services.IShelterService;
|
||||
import net.typeblog.shelter.services.ShelterService;
|
||||
import net.typeblog.shelter.util.ApplicationInfoWrapper;
|
||||
|
||||
public class AppListFragment extends Fragment {
|
||||
private static final String BROADCAST_REFRESH = "net.typeblog.shelter.broadcast.REFRESH";
|
||||
|
||||
private IShelterService mService = null;
|
||||
private boolean mIsRemote = false;
|
||||
private Drawable mDefaultIcon = null;
|
||||
private ApplicationInfoWrapper mSelectedApp = null;
|
||||
|
||||
// Views
|
||||
private RecyclerView mList = null;
|
||||
private AppListAdapter mAdapter = null;
|
||||
private SwipeRefreshLayout mSwipeRefresh = null;
|
||||
|
||||
// Receiver for Refresh events
|
||||
// used for app changes
|
||||
private BroadcastReceiver mRefreshReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (mAdapter != null) {
|
||||
mAdapter.refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static AppListFragment newInstance(IShelterService service, boolean isRemote) {
|
||||
AppListFragment fragment = new AppListFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
@ -44,6 +72,24 @@ public class AppListFragment extends Fragment {
|
|||
mIsRemote = getArguments().getBoolean("is_remote");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LocalBroadcastManager.getInstance(getContext())
|
||||
.registerReceiver(mRefreshReceiver, new IntentFilter(BROADCAST_REFRESH));
|
||||
if (mAdapter != null) {
|
||||
mAdapter.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mSelectedApp = null;
|
||||
LocalBroadcastManager.getInstance(getContext())
|
||||
.unregisterReceiver(mRefreshReceiver);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
@ -53,13 +99,68 @@ public class AppListFragment extends Fragment {
|
|||
mList = view.findViewById(R.id.fragment_list_recycler_view);
|
||||
mSwipeRefresh = view.findViewById(R.id.fragment_swipe_refresh);
|
||||
mAdapter = new AppListAdapter(mService, mDefaultIcon, mSwipeRefresh);
|
||||
mAdapter.setContextMenuHandler((info, v) -> {
|
||||
mSelectedApp = info;
|
||||
mList.showContextMenuForChild(v);
|
||||
});
|
||||
mList.setAdapter(mAdapter);
|
||||
mList.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
mList.setHasFixedSize(true);
|
||||
mAdapter.refresh();
|
||||
|
||||
mSwipeRefresh.setOnRefreshListener(mAdapter::refresh);
|
||||
registerForContextMenu(mList);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
if (mIsRemote) {
|
||||
inflater.inflate(R.menu.menu_work, menu);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_main, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (mSelectedApp == null) return false;
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.main_clone_to_work:
|
||||
case R.id.work_clone_to_main:
|
||||
// Make a local copy
|
||||
final ApplicationInfoWrapper app = mSelectedApp;
|
||||
mSelectedApp = null;
|
||||
try {
|
||||
((MainActivity) getActivity()).getOtherService(mIsRemote)
|
||||
.installApp(app, new IAppInstallCallback.Stub() {
|
||||
@Override
|
||||
public void callback(int result) {
|
||||
installAppCallback(result, app);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
// TODO: Maybe tell the user?
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
void installAppCallback(int result, ApplicationInfoWrapper app) {
|
||||
if (result == Activity.RESULT_OK) {
|
||||
String message = getString(R.string.clone_success);
|
||||
message = String.format(message, app.mLabel);
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
LocalBroadcastManager.getInstance(getContext())
|
||||
.sendBroadcast(new Intent(BROADCAST_REFRESH));
|
||||
} else if (result == ShelterService.RESULT_CANNOT_INSTALL_SYSTEM_APP) {
|
||||
Toast.makeText(getContext(),
|
||||
getString(R.string.clone_fail_system_app), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,30 +5,47 @@ import android.app.admin.DevicePolicyManager;
|
|||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.StrictMode;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.Toast;
|
||||
|
||||
import net.typeblog.shelter.R;
|
||||
import net.typeblog.shelter.ShelterApplication;
|
||||
import net.typeblog.shelter.services.IAppInstallCallback;
|
||||
import net.typeblog.shelter.util.Utility;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
// DummyActivity does nothing about presenting any UI
|
||||
// It is a wrapper over various different operations
|
||||
// that might be required to perform across user profiles
|
||||
// which is only possible through Intents that are in
|
||||
// the crossProfileIntentFilter
|
||||
public class DummyActivity extends Activity {
|
||||
public static final String FINALIZE_PROVISION = "net.typeblog.shelter.action.FINALIZE_PROVISION";
|
||||
public static final String START_SERVICE = "net.typeblog.shelter.action.START_SERVICE";
|
||||
public static final String TRY_START_SERVICE = "net.typeblog.shelter.action.TRY_START_SERVICE";
|
||||
public static final String INSTALL_PACKAGE = "net.typeblog.shelter.action.INSTALL_PACKAGE";
|
||||
|
||||
private static final int REQUEST_INSTALL_PACKAGE = 1;
|
||||
|
||||
private boolean mIsProfileOwner = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getSystemService(DevicePolicyManager.class).isProfileOwnerApp(getPackageName())) {
|
||||
mIsProfileOwner = getSystemService(DevicePolicyManager.class).isProfileOwnerApp(getPackageName());
|
||||
if (mIsProfileOwner) {
|
||||
// If we are the profile owner, we enforce all our policies
|
||||
// so that we can make sure those are updated with our app
|
||||
Utility.enforceWorkProfilePolicies(this);
|
||||
Utility.enforceUserRestrictions(this);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
@ -39,9 +56,30 @@ public class DummyActivity extends Activity {
|
|||
// This is used for testing if work mode is disabled from MainActivity
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
} else if (INSTALL_PACKAGE.equals(intent.getAction())) {
|
||||
actionInstallPackage();
|
||||
} else if (FINALIZE_PROVISION.equals(intent.getAction())) {
|
||||
actionFinalizeProvision();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_INSTALL_PACKAGE) {
|
||||
appInstallFinished(resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void actionFinalizeProvision() {
|
||||
// This is the action used by DeviceAdminReceiver to finalize the setup
|
||||
// The work has been finished in onCreate(), now we just have to
|
||||
// inform the user to restart the main activity
|
||||
Toast.makeText(this, getString(R.string.provision_finished), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void actionStartService() {
|
||||
((ShelterApplication) getApplication()).bindShelterService(new ServiceConnection() {
|
||||
@Override
|
||||
|
@ -60,4 +98,44 @@ public class DummyActivity extends Activity {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void actionInstallPackage() {
|
||||
Uri uri = Uri.fromParts("package", getIntent().getStringExtra("package"), null);
|
||||
StrictMode.VmPolicy policy = StrictMode.getVmPolicy();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// I really have no idea about why the "package:" uri do not work
|
||||
// after Android O, anyway we fall back to using the apk path...
|
||||
// Since I have plan to support pre-O in later versions, I keep this
|
||||
// branch in case that we reduce minSDK in the future.
|
||||
uri = Uri.fromFile(new File(getIntent().getStringExtra("apk")));
|
||||
|
||||
// A permissive VmPolicy must be set to work around
|
||||
// the limitation on cross-application Uri
|
||||
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE, uri);
|
||||
intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, getPackageName());
|
||||
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||
startActivityForResult(intent, REQUEST_INSTALL_PACKAGE);
|
||||
|
||||
// Restore the VmPolicy anyway
|
||||
StrictMode.setVmPolicy(policy);
|
||||
}
|
||||
|
||||
private void appInstallFinished(int resultCode) {
|
||||
// Send the result code back to the caller
|
||||
Bundle callbackExtra = getIntent().getBundleExtra("callback");
|
||||
IAppInstallCallback callback = IAppInstallCallback.Stub
|
||||
.asInterface(callbackExtra.getBinder("callback"));
|
||||
|
||||
try {
|
||||
callback.callback(resultCode);
|
||||
} catch (RemoteException e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package net.typeblog.shelter.ui;
|
||||
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -172,6 +169,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
mTabs.setupWithViewPager(mPager);
|
||||
}
|
||||
|
||||
// Get the service on the other side
|
||||
// remote (work) -> main
|
||||
// main -> remote (work)
|
||||
IShelterService getOtherService(boolean isRemote) {
|
||||
return isRemote ? mServiceMain : mServiceWork;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
@ -194,20 +198,17 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_PROVISION_PROFILE) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
// The sync part of the setup process is completed
|
||||
// We register a receiver to wait for the async part
|
||||
registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
unregisterReceiver(this); // We only want to receive this once
|
||||
mStorage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
|
||||
bindServices();
|
||||
}
|
||||
}, new IntentFilter(DevicePolicyManager.ACTION_MANAGED_PROFILE_PROVISIONED));
|
||||
// Let's just assume it succeeded. If it did not, the program will break
|
||||
// on the next start anyway.
|
||||
mStorage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
|
||||
|
||||
// However, we still have to wait for DummyActivity in work profile to finish
|
||||
Toast.makeText(this,
|
||||
getString(R.string.provision_still_pending), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.work_profile_provision_failed), Toast.LENGTH_LONG).show();
|
||||
|
@ -240,6 +241,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
Toast.makeText(this, getString(R.string.device_admin_toast), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.graphics.Bitmap;
|
|||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.UserManager;
|
||||
|
||||
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
|
||||
import net.typeblog.shelter.services.IShelterService;
|
||||
|
@ -55,6 +56,15 @@ public class Utility {
|
|||
adminComponent,
|
||||
new IntentFilter(DummyActivity.TRY_START_SERVICE),
|
||||
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
||||
|
||||
manager.setProfileEnabled(adminComponent);
|
||||
}
|
||||
|
||||
public static void enforceUserRestrictions(Context context) {
|
||||
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);
|
||||
ComponentName adminComponent = new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class);
|
||||
manager.clearUserRestriction(adminComponent, UserManager.DISALLOW_INSTALL_APPS);
|
||||
manager.clearUserRestriction(adminComponent, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
|
||||
}
|
||||
|
||||
// From <https://stackoverflow.com/questions/3035692/how-to-convert-a-drawable-to-a-bitmap>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp">
|
||||
|
||||
|
|
6
app/src/main/res/menu/menu_main.xml
Normal file
6
app/src/main/res/menu/menu_main.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/main_clone_to_work"
|
||||
android:title="@string/clone_to_work_profile" />
|
||||
</menu>
|
6
app/src/main/res/menu/menu_work.xml
Normal file
6
app/src/main/res/menu/menu_work.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/work_clone_to_main"
|
||||
android:title="@string/clone_to_main_profile" />
|
||||
</menu>
|
|
@ -2,8 +2,10 @@
|
|||
<string name="app_name">Shelter</string>
|
||||
<string name="device_admin_label">Shelter</string>
|
||||
<string name="device_admin_desc">App Isolation Service</string>
|
||||
<string name="device_admin_explanation">Shelter needs to become Device Admin in order to perform its isolation tasks.</string>\
|
||||
<string name="device_admin_explanation">Shelter needs to become Device Admin in order to perform its isolation tasks.</string>
|
||||
<string name="device_admin_toast">You have to grant Device Admin permission for Shelter to work. Please try again.</string>
|
||||
<string name="provision_still_pending">Please wait while we prepare Shelter profile for you …</string>
|
||||
<string name="provision_finished">Shelter setup complete. Please restart Shelter.</string>
|
||||
<string name="msg_device_unsupported">Permission is denied or Unsupported device</string>
|
||||
<string name="work_profile_not_found">Work profile not found. Please restart the app to re-provision the profile.</string>
|
||||
<string name="work_profile_provision_failed">Cannot provision work profile. You may try again by restarting Shelter.</string>
|
||||
|
@ -13,4 +15,8 @@
|
|||
<string name="fragment_profile_main">Main</string>
|
||||
<string name="fragment_profile_work">Shelter</string>
|
||||
<string name="list_item_disabled">[Frozen] %s</string>
|
||||
<string name="clone_to_work_profile">Clone to Shelter (Work Profile)</string>
|
||||
<string name="clone_to_main_profile">Clone to Main Profile</string>
|
||||
<string name="clone_success">Application "%s" cloned successfully</string>
|
||||
<string name="clone_fail_system_app">Cannot clone system apps to a profile that Shelter has no control of.</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue