implement direct APK installation
This commit is contained in:
parent
03c5224dda
commit
17da5e8dc5
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal 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>
|
Loading…
Reference in a new issue