diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e30f11a..dea71b9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,6 +67,17 @@
+
+
+
+
+
= 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")));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O || getIntent().hasExtra("direct_install_apk")) {
+ if (getIntent().hasExtra("apk")) {
+ // 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")));
+ } else if (getIntent().hasExtra("direct_install_apk")) {
+ // Directly install an APK inside the profile
+ // The APK will be an Uri from our own FileProviderProxy
+ // which points to an opened Fd in another profile.
+ // We must close the Fd when we finish.
+ uri = getIntent().getParcelableExtra("direct_install_apk");
+ }
// A permissive VmPolicy must be set to work around
// the limitation on cross-application Uri
@@ -155,6 +166,7 @@ public class DummyActivity extends Activity {
intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, getPackageName());
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, REQUEST_INSTALL_PACKAGE);
// Restore the VmPolicy anyway
@@ -175,6 +187,13 @@ public class DummyActivity extends Activity {
}
private void appInstallFinished(int resultCode) {
+ // Clear the fd anyway since we have finished installation.
+ // Because we might have been installing an APK opened from
+ // the other profile. We don't know, but just clean it.
+ FileProviderProxy.clearFd();
+
+ if (!getIntent().hasExtra("callback")) return;
+
// Send the result code back to the caller
Bundle callbackExtra = getIntent().getBundleExtra("callback");
IAppInstallCallback callback = IAppInstallCallback.Stub
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 e3fd248..5630142 100644
--- a/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java
+++ b/app/src/main/java/net/typeblog/shelter/ui/MainActivity.java
@@ -5,7 +5,10 @@ import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
@@ -24,11 +27,14 @@ 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.IAppInstallCallback;
import net.typeblog.shelter.services.IShelterService;
import net.typeblog.shelter.services.KillerService;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
+import java.io.IOException;
+
public class MainActivity extends AppCompatActivity {
public static final String BROADCAST_CONTEXT_MENU_CLOSED = "net.typeblog.shelter.broadcast.CONTEXT_MENU_CLOSED";
@@ -36,6 +42,7 @@ public class MainActivity extends AppCompatActivity {
private static final int REQUEST_START_SERVICE_IN_WORK_PROFILE = 2;
private static final int REQUEST_SET_DEVICE_ADMIN = 3;
private static final int REQUEST_TRY_START_SERVICE_IN_WORK_PROFILE = 4;
+ private static final int REQUEST_DOCUMENTS_CHOOSE_APK = 5;
private LocalStorageManager mStorage = null;
private DevicePolicyManager mPolicyManager = null;
@@ -328,6 +335,12 @@ public class MainActivity extends AppCompatActivity {
Icon.createWithResource(this, R.mipmap.ic_freeze),
"shelter-freeze-all", getString(R.string.freeze_all_shortcut));
return true;
+ case R.id.main_menu_install_app_to_profile:
+ Intent openApkIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ openApkIntent.addCategory(Intent.CATEGORY_OPENABLE);
+ openApkIntent.setType("application/vnd.android.package-archive");
+ startActivityForResult(openApkIntent, REQUEST_DOCUMENTS_CHOOSE_APK);
+ return true;
}
return super.onOptionsItemSelected(item);
}
@@ -383,6 +396,25 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, getString(R.string.device_admin_toast), Toast.LENGTH_LONG).show();
finish();
}
+ } else if (requestCode == REQUEST_DOCUMENTS_CHOOSE_APK && resultCode == RESULT_OK && data != null) {
+ Uri uri = data.getData();
+
+ try {
+ ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(uri, "r");
+ mServiceWork.installApk(fd, new IAppInstallCallback.Stub() {
+ @Override
+ public void callback(int result) {
+ runOnUiThread(() -> {
+ // The other side will have closed the Fd for us
+ if (result == RESULT_OK)
+ Toast.makeText(MainActivity.this,
+ R.string.install_app_to_profile_success, Toast.LENGTH_LONG).show();
+ });
+ }
+ });
+ } catch (RemoteException | IOException e) {
+ // Well, I don't know what to do then
+ }
} else {
super.onActivityResult(requestCode, resultCode, data);
}
diff --git a/app/src/main/java/net/typeblog/shelter/util/FileProviderProxy.java b/app/src/main/java/net/typeblog/shelter/util/FileProviderProxy.java
new file mode 100644
index 0000000..55cc6e1
--- /dev/null
+++ b/app/src/main/java/net/typeblog/shelter/util/FileProviderProxy.java
@@ -0,0 +1,56 @@
+package net.typeblog.shelter.util;
+
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
+import android.support.v4.content.FileProvider;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// A simple and naïve FileProvider which forwards content Uris
+// to a given Fd, or fallback to default FileProvider if no Fd is present.
+// This is used to work around the limitations of content Uris, which
+// can only be opened from the process that was granted the read permission.
+// We can send the Fd through AIDL and use this FileProviderProxy to re-generate
+// another content Uri that points to the same Fd.
+public class FileProviderProxy extends FileProvider {
+ private static final String AUTHORITY_NAME = "net.typeblog.shelter.files";
+ private static final String FORWARD_PATH_PREFIX = "/forward/"; // All content Uris pointing to this path indicates forwarded Fd.
+ private static ParcelFileDescriptor sFd = null;
+
+ // Register the fd to be forwarded
+ // This will close the last Fd that we have set
+ // Returns the content Uri to be used.
+ public static Uri setFd(ParcelFileDescriptor fd, String suffix) {
+ clearFd();
+ sFd = fd;
+ return Uri.parse("content://" + AUTHORITY_NAME + FORWARD_PATH_PREFIX + "temp." + suffix);
+ }
+
+ // Close and delete the current Fd to be forwarded.
+ public static void clearFd() {
+ if (sFd == null) return;
+
+ try {
+ sFd.close();
+ } catch (IOException e) {
+ // ...
+ }
+
+ sFd = null;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+ if (uri.getPath().startsWith(FORWARD_PATH_PREFIX) && sFd != null) {
+ // If we are now in the FORWARD_PATH_PREFIX
+ // We just return the Fd that was registered to be forwarded
+ ParcelFileDescriptor fd = sFd;
+ sFd = null;
+ return fd;
+ } else {
+ return super.openFile(uri, mode);
+ }
+ }
+}
diff --git a/app/src/main/res/menu/main_activity_menu.xml b/app/src/main/res/menu/main_activity_menu.xml
index 4a25191..e4abe1c 100644
--- a/app/src/main/res/menu/main_activity_menu.xml
+++ b/app/src/main/res/menu/main_activity_menu.xml
@@ -16,4 +16,8 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index b27d105..ceffba3 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -29,6 +29,8 @@
批量冻结
创建批量冻结快捷方式
冻结
+ 安装 APK 到 Shelter
+ 已成功在工作用户内安装 APK
设置
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6f548d3..87a7228 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,6 +31,8 @@
Batch Freeze
Create Batch Freeze Shortcut
Freeze
+ Install APK into Shelter
+ Application installation finished in work profile.
Settings
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..5720da1
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file