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