Shelter: implement cross-profile interaction
This commit is contained in:
parent
dd20dd7768
commit
83ea35cfd0
|
@ -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">
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue