implement direct APK installation

This commit is contained in:
Peter Cai 2018-09-16 12:08:30 +08:00
parent 03c5224dda
commit 17da5e8dc5
No known key found for this signature in database
GPG key ID: 71F5FB4E4F3FD54F
10 changed files with 163 additions and 9 deletions

View file

@ -67,6 +67,17 @@
</intent-filter>
</receiver>
<!-- A FileProvider that proxies opened Fd from the other profile -->
<provider
android:name="net.typeblog.shelter.util.FileProviderProxy"
android:authorities="net.typeblog.shelter.files"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Core service running on both the main profile and the work profile -->
<!-- Actions like cloning / freezing apps will be performed by this service -->
<service android:name=".services.ShelterService"

View file

@ -2,6 +2,7 @@
package net.typeblog.shelter.services;
import android.content.pm.ApplicationInfo;
import android.os.ParcelFileDescriptor;
import net.typeblog.shelter.services.IAppInstallCallback;
import net.typeblog.shelter.services.IGetAppsCallback;
@ -14,6 +15,7 @@ interface IShelterService {
void getApps(IGetAppsCallback callback);
void loadIcon(in ApplicationInfoWrapper info, ILoadIconCallback callback);
void installApp(in ApplicationInfoWrapper app, IAppInstallCallback callback);
void installApk(in ParcelFileDescriptor fd, IAppInstallCallback callback);
void uninstallApp(in ApplicationInfoWrapper app, IAppInstallCallback callback);
void freezeApp(in ApplicationInfoWrapper app);
void unfreezeApp(in ApplicationInfoWrapper app);

View file

@ -12,9 +12,11 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.support.annotation.Nullable;
@ -23,9 +25,9 @@ import net.typeblog.shelter.ShelterApplication;
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
import net.typeblog.shelter.ui.DummyActivity;
import net.typeblog.shelter.util.ApplicationInfoWrapper;
import net.typeblog.shelter.util.FileProviderProxy;
import net.typeblog.shelter.util.Utility;
import java.lang.annotation.Target;
import java.util.List;
import java.util.stream.Collectors;
@ -149,6 +151,26 @@ public class ShelterService extends Service {
}
}
@Override
public void installApk(ParcelFileDescriptor fd, IAppInstallCallback callback) {
// Directly install an APK through a given Fd
// instead of installing an existing one
Intent intent = new Intent(DummyActivity.INSTALL_PACKAGE);
intent.setComponent(new ComponentName(ShelterService.this, DummyActivity.class));
// Generate a content Uri pointing to the Fd
// DummyActivity is expected to release the Fd after finishing
Uri uri = FileProviderProxy.setFd(fd, "apk");
intent.putExtra("direct_install_apk", uri);
// Send the callback to the DummyActivity
Bundle callbackExtra = new Bundle();
callbackExtra.putBinder("callback", callback.asBinder());
intent.putExtra("callback", callbackExtra);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
}
@Override
public void uninstallApp(ApplicationInfoWrapper app, IAppInstallCallback callback) throws RemoteException {
if (!app.isSystem()) {

View file

@ -8,7 +8,6 @@ import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.StrictMode;
@ -19,6 +18,7 @@ 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.util.FileProviderProxy;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
@ -137,14 +137,25 @@ public class DummyActivity extends Activity {
}
private void actionInstallPackage() {
Uri uri = Uri.fromParts("package", getIntent().getStringExtra("package"), null);
Uri uri = null;
if (getIntent().hasExtra("package")) {
uri = Uri.fromParts("package", getIntent().getStringExtra("package"), null);
}
StrictMode.VmPolicy policy = StrictMode.getVmPolicy();
if (Build.VERSION.SDK_INT >= 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

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -16,4 +16,8 @@
<item
android:id="@+id/main_menu_create_freeze_all_shortcut"
android:title="@string/create_freeze_all_shortcut" />
<item
android:id="@+id/main_menu_install_app_to_profile"
android:title="@string/install_app_to_profile" />
</menu>

View file

@ -29,6 +29,8 @@
<string name="freeze_all">批量冻结</string>
<string name="create_freeze_all_shortcut">创建批量冻结快捷方式</string>
<string name="freeze_all_shortcut">冻结</string>
<string name="install_app_to_profile">安装 APK 到 Shelter</string>
<string name="install_app_to_profile_success">已成功在工作用户内安装 APK</string>
<!-- Settings Options -->
<string name="settings">设置</string>

View file

@ -31,6 +31,8 @@
<string name="freeze_all">Batch Freeze</string>
<string name="create_freeze_all_shortcut">Create Batch Freeze Shortcut</string>
<string name="freeze_all_shortcut">Freeze</string>
<string name="install_app_to_profile">Install APK into Shelter</string>
<string name="install_app_to_profile_success">Application installation finished in work profile.</string>
<!-- Settings Options -->
<string name="settings">Settings</string>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Nothing here yet. Our FileProvider is for now merely a proxy -->
</paths>