Shelter: implement cross-profile interaction

This commit is contained in:
Peter Cai 2018-08-20 16:10:31 +08:00
parent dd20dd7768
commit 83ea35cfd0
No known key found for this signature in database
GPG Key ID: 71F5FB4E4F3FD54F
10 changed files with 252 additions and 14 deletions

View File

@ -25,7 +25,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -4,7 +4,7 @@ android {
compileSdkVersion 28
defaultConfig {
applicationId "net.typeblog.shelter"
minSdkVersion 24
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
@ -16,10 +16,14 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'

View File

@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.typeblog.shelter">
<uses-feature android:name="android.software.device_admin" android:required="true"/>
<uses-feature android:name="android.software.managed_users" android:required="true"/>
<!--<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>-->
<application
android:name=".ShelterApplication"
android:allowBackup="false"
@ -17,6 +21,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.DummyActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="net.typeblog.shelter.action.START_SERVICE"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<receiver android:name=".receivers.ShelterDeviceAdminReceiver"
android:label="@string/device_admin_label"
android:description="@string/device_admin_desc"
@ -27,6 +38,9 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<service android:name=".services.ShelterService"
android:exported="true"
android:permission="android.permission.BIND_DEVICE_ADMIN"/>
</application>
</manifest>

View File

@ -0,0 +1,9 @@
// IShelterService.aidl
package net.typeblog.shelter.services;
import android.content.pm.ResolveInfo;
interface IShelterService {
void stopShelterService(boolean kill);
List<ResolveInfo> getApps();
}

View File

@ -1,13 +1,34 @@
package net.typeblog.shelter;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import net.typeblog.shelter.services.ShelterService;
import net.typeblog.shelter.util.LocalStorageManager;
public class ShelterApplication extends Application {
private ServiceConnection mShelterServiceConnection = null;
@Override
public void onCreate() {
super.onCreate();
LocalStorageManager.initialize(this);
}
public void bindShelterService(ServiceConnection conn) {
unbindShelterService();
Intent intent = new Intent(getApplicationContext(), ShelterService.class);
bindService(intent, conn, Context.BIND_AUTO_CREATE);
mShelterServiceConnection = conn;
}
public void unbindShelterService() {
if (mShelterServiceConnection != null) {
unbindService(mShelterServiceConnection);
}
mShelterServiceConnection = null;
}
}

View File

@ -5,6 +5,7 @@ import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import net.typeblog.shelter.ui.MainActivity;
@ -26,14 +27,21 @@ 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
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);
manager.setProfileEnabled(new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class));
manager.setProfileEnabled(adminComponent);
// Hide this app in the work profile
context.getPackageManager().setComponentEnabledSetting(
new ComponentName(context.getApplicationContext(), MainActivity.class),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0);
// Allow cross-profile intents for START_SERVICE
manager.addCrossProfileIntentFilter(
adminComponent,
new IntentFilter("net.typeblog.shelter.action.START_SERVICE"),
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
}
}

View File

@ -0,0 +1,56 @@
package net.typeblog.shelter.services;
import android.app.Service;
import android.app.admin.DevicePolicyManager;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.support.annotation.Nullable;
import net.typeblog.shelter.ShelterApplication;
import java.util.List;
public class ShelterService extends Service {
private DevicePolicyManager mPolicyManager = null;
private boolean mIsWorkProfile = false;
private IShelterService.Stub mBinder = new IShelterService.Stub() {
@Override
public void stopShelterService(boolean kill) {
// dirty: just wait for some time and kill this service itself
new Thread(() -> {
try {
Thread.sleep(1);
} catch (Exception e) {
}
((ShelterApplication) getApplication()).unbindShelterService();
if (kill) {
// Just kill the entire process if this signal is received
System.exit(0);
}
}).start();
}
@Override
public List<ResolveInfo> getApps() {
Intent mainIntent = new Intent(Intent.ACTION_MAIN);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
return getPackageManager().queryIntentActivities(mainIntent, 0);
}
};
@Override
public void onCreate() {
mPolicyManager = getSystemService(DevicePolicyManager.class);
mIsWorkProfile = mPolicyManager.isProfileOwnerApp(getPackageName());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View File

@ -0,0 +1,35 @@
package net.typeblog.shelter.ui;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;
import net.typeblog.shelter.ShelterApplication;
public class DummyActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((ShelterApplication) getApplication()).bindShelterService(new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Intent data = new Intent();
Bundle bundle = new Bundle();
bundle.putBinder("service", service);
data.putExtra("extra", bundle);
setResult(RESULT_OK, data);
finish();
}
@Override
public void onServiceDisconnected(ComponentName name) {
// dummy
}
});
}
}

View File

@ -3,21 +3,32 @@ package net.typeblog.shelter.ui;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
import net.typeblog.shelter.R;
import net.typeblog.shelter.ShelterApplication;
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
import net.typeblog.shelter.services.IShelterService;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_PROVISION_PROFILE = 1;
private static final int REQUEST_START_SERVICE_IN_WORK_PROFILE = 2;
private LocalStorageManager mStorage = null;
private DevicePolicyManager mPolicyManager = null;
// Two services running in main / work profile
private IShelterService mServiceMain = null;
private IShelterService mServiceWork = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -28,6 +39,7 @@ public class MainActivity extends AppCompatActivity {
if (mPolicyManager.isProfileOwnerApp(getPackageName())) {
// We are now in our own profile
// We should never start the main activity here.
android.util.Log.d("MainActivity", "started in user profile. stopping.");
finish();
} else {
if (!mStorage.getBoolean(LocalStorageManager.PREF_IS_DEVICE_ADMIN)) {
@ -39,14 +51,13 @@ public class MainActivity extends AppCompatActivity {
setupProfile();
} else {
// Initialize the app
// we should bind to a service running in the work profile
// in order to get the application lists etc.
initializeApp();
}
}
}
private boolean setupProfile() {
private void setupProfile() {
// Check if provisioning is allowed
if (!mPolicyManager.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)) {
Toast.makeText(this,
@ -55,27 +66,85 @@ public class MainActivity extends AppCompatActivity {
}
// Start provisioning
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,
new ComponentName(getApplicationContext(), ShelterDeviceAdminReceiver.class));
intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, admin);
startActivityForResult(intent, REQUEST_PROVISION_PROFILE);
}
// We should continue the setup process later when provision completed
return false;
private void initializeApp() {
// Bind to the service provided by this app in main user
((ShelterApplication) getApplication()).bindShelterService(new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mServiceMain = IShelterService.Stub.asInterface(service);
bindWorkService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
// dummy
}
});
}
private void bindWorkService() {
// Bind to the ShelterService in work profile
Intent intent = new Intent("net.typeblog.shelter.action.START_SERVICE");
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
Utility.transferIntentToProfile(this, intent);
startActivityForResult(intent, REQUEST_START_SERVICE_IN_WORK_PROFILE);
}
private void buildView() {
// Finally we can build the view
// TODO: Actually implement this method
try {
android.util.Log.d("MainActivity", "Main profile app count: " + mServiceMain.getApps().size());
android.util.Log.d("MainActivity", "Work profile app count: " + mServiceWork.getApps().size());
} catch (Exception e) {
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
// For the work instance, we just kill it entirely
// We don't need it to be awake to do anything useful
mServiceWork.stopShelterService(true);
} catch (RemoteException e) {
// We are stopping anyway
}
try {
mServiceMain.stopShelterService(false);
} catch (RemoteException e) {
// We are stopping anyway
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == REQUEST_PROVISION_PROFILE) {
if (requestCode == REQUEST_PROVISION_PROFILE && resultCode == RESULT_OK) {
// Provisioning finished.
// Set the HAS_SETUP flag
mStorage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
// Initialize the app just as if the activity was started.
// TODO: Should not initialize here. It is possible that the process is not finished yet.
//initializeApp();
} else if (requestCode == REQUEST_START_SERVICE_IN_WORK_PROFILE && resultCode == RESULT_OK) {
// TODO: Set the service in work profile as foreground to keep it alive
Bundle extra = data.getBundleExtra("extra");
IBinder binder = extra.getBinder("service");
mServiceWork = IShelterService.Stub.asInterface(binder);
buildView();
}
}
}

View File

@ -0,0 +1,22 @@
package net.typeblog.shelter.util;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import java.util.List;
import java.util.stream.Collectors;
public class Utility {
// Affiliate an Intent to another profile (i.e. the Work profile that we manage)
public static void transferIntentToProfile(Context context, Intent intent) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> info = pm.queryIntentActivities(intent, 0);
ResolveInfo i = info.stream()
.filter((r) -> !r.activityInfo.packageName.equals(context.getPackageName()))
.collect(Collectors.toList()).get(0);
intent.setComponent(new ComponentName(i.activityInfo.packageName, i.activityInfo.name));
}
}