295 lines
12 KiB
Java
295 lines
12 KiB
Java
package net.typeblog.shelter.services;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.app.Activity;
|
|
import android.app.Notification;
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationManager;
|
|
import android.app.Service;
|
|
import android.app.admin.DevicePolicyManager;
|
|
import android.content.ComponentName;
|
|
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.RemoteException;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import net.typeblog.shelter.R;
|
|
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.UriForwardProxy;
|
|
import net.typeblog.shelter.util.Utility;
|
|
|
|
import java.util.List;
|
|
import java.util.stream.Collectors;
|
|
|
|
public class ShelterService extends Service {
|
|
public static final int RESULT_CANNOT_INSTALL_SYSTEM_APP = 100001;
|
|
|
|
private static final String NOTIFICATION_CHANNEL_ID = "ShelterService";
|
|
private DevicePolicyManager mPolicyManager = null;
|
|
private boolean mIsProfileOwner = false;
|
|
private PackageManager mPackageManager = null;
|
|
private ComponentName mAdminComponent = null;
|
|
private IShelterService.Stub mBinder = new IShelterService.Stub() {
|
|
@Override
|
|
public void ping() {
|
|
// Do nothing, just let the other side know we are alive
|
|
}
|
|
|
|
@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 void getApps(IGetAppsCallback callback) {
|
|
new Thread(() -> {
|
|
int pmFlags = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_UNINSTALLED_PACKAGES;
|
|
List<ApplicationInfoWrapper> list = mPackageManager.getInstalledApplications(pmFlags)
|
|
.stream()
|
|
.filter((it) -> !it.packageName.equals(getPackageName()))
|
|
.filter((it) -> {
|
|
boolean isSystem = (it.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
|
boolean isHidden = isHidden(it.packageName);
|
|
boolean isInstalled = (it.flags & ApplicationInfo.FLAG_INSTALLED) != 0;
|
|
boolean canLaunch = mPackageManager.getLaunchIntentForPackage(it.packageName) != null;
|
|
|
|
return (!isSystem && isInstalled) || isHidden || canLaunch;
|
|
})
|
|
.map(ApplicationInfoWrapper::new)
|
|
.map((it) -> it.loadLabel(mPackageManager)
|
|
.setHidden(isHidden(it.getPackageName())))
|
|
.sorted((x, y) -> {
|
|
// Sort hidden apps at the last
|
|
if (x.isHidden() && !y.isHidden()) {
|
|
return 1;
|
|
} else if (!x.isHidden() && y.isHidden()) {
|
|
return -1;
|
|
} else {
|
|
return x.getLabel().compareTo(y.getLabel());
|
|
}
|
|
})
|
|
.collect(Collectors.toList());
|
|
|
|
try {
|
|
callback.callback(list);
|
|
} catch (RemoteException e) {
|
|
// Do Nothing
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
@Override
|
|
public void loadIcon(ApplicationInfoWrapper info, ILoadIconCallback callback) {
|
|
new Thread(() -> {
|
|
Bitmap icon = Utility.drawableToBitmap(info.getInfo().loadUnbadgedIcon(mPackageManager));
|
|
|
|
try {
|
|
callback.callback(icon);
|
|
} catch (RemoteException e) {
|
|
// Do Nothing
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
@Override
|
|
public void installApp(ApplicationInfoWrapper app, IAppInstallCallback callback) throws RemoteException {
|
|
if (!app.isSystem()) {
|
|
// Installing a non-system app requires firing up PackageInstaller
|
|
// Delegate this operation to DummyActivity because
|
|
// Only it can receive a result
|
|
Intent intent = new Intent(DummyActivity.INSTALL_PACKAGE);
|
|
intent.setComponent(new ComponentName(ShelterService.this, DummyActivity.class));
|
|
intent.putExtra("package", app.getPackageName());
|
|
intent.putExtra("apk", app.getSourceDir());
|
|
|
|
// 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);
|
|
DummyActivity.registerSameProcessRequest(intent);
|
|
startActivity(intent);
|
|
} else {
|
|
if (mIsProfileOwner) {
|
|
// We can only enable system apps in our own profile
|
|
mPolicyManager.enableSystemApp(
|
|
mAdminComponent,
|
|
app.getPackageName());
|
|
|
|
// Also set the hidden state to false.
|
|
mPolicyManager.setApplicationHidden(
|
|
mAdminComponent,
|
|
app.getPackageName(), false);
|
|
|
|
callback.callback(Activity.RESULT_OK);
|
|
} else {
|
|
callback.callback(RESULT_CANNOT_INSTALL_SYSTEM_APP);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void installApk(UriForwardProxy uriForwarder, 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.setUriForwardProxy(uriForwarder, "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);
|
|
DummyActivity.registerSameProcessRequest(intent);
|
|
startActivity(intent);
|
|
}
|
|
|
|
@Override
|
|
public void uninstallApp(ApplicationInfoWrapper app, IAppInstallCallback callback) throws RemoteException {
|
|
if (!app.isSystem()) {
|
|
// Similarly, fire up DummyActivity to do uninstallation for us
|
|
Intent intent = new Intent(DummyActivity.UNINSTALL_PACKAGE);
|
|
intent.setComponent(new ComponentName(ShelterService.this, DummyActivity.class));
|
|
intent.putExtra("package", app.getPackageName());
|
|
|
|
// 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);
|
|
DummyActivity.registerSameProcessRequest(intent);
|
|
startActivity(intent);
|
|
} else {
|
|
if (mIsProfileOwner) {
|
|
// This is essentially the same as disabling the system app
|
|
// There is no way to reverse the "enableSystemApp" operation here
|
|
mPolicyManager.setApplicationHidden(
|
|
mAdminComponent,
|
|
app.getPackageName(), true);
|
|
callback.callback(Activity.RESULT_OK);
|
|
} else {
|
|
callback.callback(RESULT_CANNOT_INSTALL_SYSTEM_APP);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void freezeApp(ApplicationInfoWrapper app) {
|
|
if (!mIsProfileOwner)
|
|
throw new IllegalArgumentException("Cannot freeze app without being profile owner");
|
|
|
|
mPolicyManager.setApplicationHidden(
|
|
mAdminComponent,
|
|
app.getPackageName(), true);
|
|
}
|
|
|
|
@Override
|
|
public void unfreezeApp(ApplicationInfoWrapper app) {
|
|
if (!mIsProfileOwner)
|
|
throw new IllegalArgumentException("Cannot unfreeze app without being profile owner");
|
|
|
|
mPolicyManager.setApplicationHidden(
|
|
mAdminComponent,
|
|
app.getPackageName(), false);
|
|
}
|
|
};
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
mPolicyManager = getSystemService(DevicePolicyManager.class);
|
|
mPackageManager = getPackageManager();
|
|
mIsProfileOwner = mPolicyManager.isProfileOwnerApp(getPackageName());
|
|
mAdminComponent = new ComponentName(getApplicationContext(), ShelterDeviceAdminReceiver.class);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
if (intent.getBooleanExtra("foreground", false)) {
|
|
setForeground();
|
|
}
|
|
return mBinder;
|
|
}
|
|
|
|
private boolean isHidden(String packageName) {
|
|
return mIsProfileOwner && mPolicyManager.isApplicationHidden(mAdminComponent, packageName);
|
|
}
|
|
|
|
private void setForeground() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
setForegroundOreo();
|
|
} else {
|
|
setForegroundLollipop();
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
private void setForegroundLollipop() {
|
|
Notification notification = new Notification.Builder(this)
|
|
.setTicker(getString(R.string.app_name))
|
|
.setContentTitle(getString(R.string.service_title))
|
|
.setContentText(getString(R.string.service_desc))
|
|
.setSmallIcon(R.drawable.ic_notification_white_24dp)
|
|
.build();
|
|
startForeground(1, notification);
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
|
private void setForegroundOreo() {
|
|
// Android O and later: Notification Channel
|
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
|
if (nm.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
|
|
NotificationChannel chan = new NotificationChannel(
|
|
NOTIFICATION_CHANNEL_ID, getString(R.string.app_name),
|
|
NotificationManager.IMPORTANCE_LOW);
|
|
nm.createNotificationChannel(chan);
|
|
}
|
|
|
|
// Disable everything: do not disturb the user
|
|
NotificationChannel chan = nm.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
|
|
chan.enableVibration(false);
|
|
chan.enableLights(false);
|
|
chan.setImportance(NotificationManager.IMPORTANCE_LOW);
|
|
nm.createNotificationChannel(chan);
|
|
|
|
// Create foreground notification to keep the service alive
|
|
Notification notification = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
|
|
.setTicker(getString(R.string.app_name))
|
|
.setContentTitle(getString(R.string.service_title))
|
|
.setContentText(getString(R.string.service_desc))
|
|
.setSmallIcon(R.drawable.ic_notification_white_24dp)
|
|
.build();
|
|
startForeground(1, notification);
|
|
}
|
|
}
|