Shelter/app/src/main/java/net/typeblog/shelter/services/ShelterService.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);
}
}