diff --git a/.idea/misc.xml b/.idea/misc.xml index 99202cc..c0f68ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -25,7 +25,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 2abe322..102ad17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a123e18..207c365 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/aidl/net/typeblog/shelter/services/IShelterService.aidl b/app/src/main/aidl/net/typeblog/shelter/services/IShelterService.aidl new file mode 100644 index 0000000..3de3d54 --- /dev/null +++ b/app/src/main/aidl/net/typeblog/shelter/services/IShelterService.aidl @@ -0,0 +1,9 @@ +// IShelterService.aidl +package net.typeblog.shelter.services; + +import android.content.pm.ResolveInfo; + +interface IShelterService { + void stopShelterService(boolean kill); + List getApps(); +} diff --git a/app/src/main/java/net/typeblog/shelter/ShelterApplication.java b/app/src/main/java/net/typeblog/shelter/ShelterApplication.java index 5f04113..1ab93c2 100644 --- a/app/src/main/java/net/typeblog/shelter/ShelterApplication.java +++ b/app/src/main/java/net/typeblog/shelter/ShelterApplication.java @@ -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; + } } diff --git a/app/src/main/java/net/typeblog/shelter/receivers/ShelterDeviceAdminReceiver.java b/app/src/main/java/net/typeblog/shelter/receivers/ShelterDeviceAdminReceiver.java index bfa5034..12991a2 100644 --- a/app/src/main/java/net/typeblog/shelter/receivers/ShelterDeviceAdminReceiver.java +++ b/app/src/main/java/net/typeblog/shelter/receivers/ShelterDeviceAdminReceiver.java @@ -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); } } diff --git a/app/src/main/java/net/typeblog/shelter/services/ShelterService.java b/app/src/main/java/net/typeblog/shelter/services/ShelterService.java new file mode 100644 index 0000000..77fa6b8 --- /dev/null +++ b/app/src/main/java/net/typeblog/shelter/services/ShelterService.java @@ -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 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; + } +} diff --git a/app/src/main/java/net/typeblog/shelter/ui/DummyActivity.java b/app/src/main/java/net/typeblog/shelter/ui/DummyActivity.java new file mode 100644 index 0000000..95535e1 --- /dev/null +++ b/app/src/main/java/net/typeblog/shelter/ui/DummyActivity.java @@ -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 + } + }); + } +} diff --git a/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java b/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java index 3cace6d..b159d94 100644 --- a/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java +++ b/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java @@ -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(); } } } diff --git a/app/src/main/java/net/typeblog/shelter/util/Utility.java b/app/src/main/java/net/typeblog/shelter/util/Utility.java new file mode 100644 index 0000000..af52455 --- /dev/null +++ b/app/src/main/java/net/typeblog/shelter/util/Utility.java @@ -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 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)); + } +}