533 lines
23 KiB
Java
533 lines
23 KiB
Java
package net.typeblog.shelter.util;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.app.AppOpsManager;
|
|
import android.app.Notification;
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.app.admin.DevicePolicyManager;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.content.pm.ShortcutInfo;
|
|
import android.content.pm.ShortcutManager;
|
|
import android.database.Cursor;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.graphics.drawable.Icon;
|
|
import android.os.Build;
|
|
import android.os.Environment;
|
|
import android.os.UserManager;
|
|
import android.provider.MediaStore;
|
|
import android.provider.Settings;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.activity.result.contract.ActivityResultContract;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import net.typeblog.shelter.R;
|
|
import net.typeblog.shelter.receivers.ShelterDeviceAdminReceiver;
|
|
import net.typeblog.shelter.services.IShelterService;
|
|
import net.typeblog.shelter.ui.DummyActivity;
|
|
import net.typeblog.shelter.ui.MainActivity;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
public class Utility {
|
|
// Determine if the current app is the owner of the current profile
|
|
// TODO: Replace all occurrences of duplicated code to call this function instead
|
|
public static boolean isProfileOwner(Context context) {
|
|
return context.getSystemService(DevicePolicyManager.class)
|
|
.isProfileOwnerApp(context.getPackageName());
|
|
}
|
|
|
|
// Polyfill for String.join
|
|
public static String stringJoin(String delimiter, String[] list) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
return String.join(delimiter, list);
|
|
} else {
|
|
if (list.length == 0) return "";
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < list.length - 1; i++) {
|
|
sb.append(list[i]).append(delimiter);
|
|
}
|
|
sb.append(list[list.length - 1]);
|
|
return sb.toString();
|
|
}
|
|
}
|
|
|
|
// Affiliate an Intent to another profile (i.e. the Work profile that we manage)
|
|
// This method cares nothing about if the other profile even exists.
|
|
// When there is no other profile, this method would just simply throw
|
|
// an IndexOutOfBoundException
|
|
// which can be caught and resolved.
|
|
public static void transferIntentToProfile(Context context, Intent intent) {
|
|
transferIntentToProfileUnsigned(context, intent);
|
|
// Add signature
|
|
AuthenticationUtility.signIntent(intent);
|
|
}
|
|
|
|
public static void transferIntentToProfileUnsigned(Context context, Intent intent) {
|
|
PackageManager pm = context.getPackageManager();
|
|
List<ResolveInfo> info = pm.queryIntentActivities(intent, 0);
|
|
Optional<ResolveInfo> i = info.stream()
|
|
.filter((r) -> !r.activityInfo.packageName.equals(context.getPackageName()))
|
|
.findFirst();
|
|
if (i.isPresent()) {
|
|
intent.setComponent(new ComponentName(i.get().activityInfo.packageName, i.get().activityInfo.name));
|
|
} else {
|
|
throw new IllegalStateException("Cannot find an intent in other profile");
|
|
}
|
|
}
|
|
|
|
// Determine if the work profile is already available
|
|
// If so, return true and set all the corresponding flags to true
|
|
// This is for scenarios where the asynchronous part of the
|
|
// setup process might be finished before the synchronous part
|
|
public static boolean isWorkProfileAvailable(Context context) {
|
|
LocalStorageManager storage = LocalStorageManager.getInstance();
|
|
Intent intent = new Intent(DummyActivity.TRY_START_SERVICE);
|
|
try {
|
|
// DO NOT sign this request, because this won't be actually sent to work profile
|
|
// If this is signed, and is the first request to be signed,
|
|
// then the other side would never receive the auth_key
|
|
Utility.transferIntentToProfileUnsigned(context, intent);
|
|
storage.setBoolean(LocalStorageManager.PREF_IS_SETTING_UP, false);
|
|
storage.setBoolean(LocalStorageManager.PREF_HAS_SETUP, true);
|
|
return true;
|
|
} catch (IllegalStateException e) {
|
|
// If any exception is thrown, this means that the profile is not available
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Enforce policies and configurations in the work profile
|
|
public static void enforceWorkProfilePolicies(Context context) {
|
|
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);
|
|
ComponentName adminComponent = new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class);
|
|
|
|
// Hide this app in the work profile
|
|
context.getPackageManager().setComponentEnabledSetting(
|
|
new ComponentName(context.getApplicationContext(), MainActivity.class),
|
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0);
|
|
|
|
// Clear everything first to ensure our policies are set properly
|
|
manager.clearCrossProfileIntentFilters(adminComponent);
|
|
|
|
// Allow cross-profile intents for START_SERVICE
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.START_SERVICE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.TRY_START_SERVICE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.UNFREEZE_AND_LAUNCH),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.FREEZE_ALL_IN_LIST),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.PUBLIC_FREEZE_ALL),
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED); // Used by FreezeService in profile
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.FINALIZE_PROVISION),
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.START_FILE_SHUTTLE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.START_FILE_SHUTTLE_2),
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.SYNCHRONIZE_PREFERENCE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
// Needed by ShelterService and has to be proxied by the MainActivity in main profile
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.INSTALL_PACKAGE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
new IntentFilter(DummyActivity.UNINSTALL_PACKAGE),
|
|
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT);
|
|
|
|
// Allow ACTION_SEND and ACTION_SEND_MULTIPLE to cross from managed to parent
|
|
// TODO: Make this configurable
|
|
IntentFilter actionSendFilter = new IntentFilter();
|
|
actionSendFilter.addAction(Intent.ACTION_SEND);
|
|
actionSendFilter.addAction(Intent.ACTION_SEND_MULTIPLE);
|
|
try {
|
|
actionSendFilter.addDataType("*/*");
|
|
} catch (IntentFilter.MalformedMimeTypeException ignored) {
|
|
// WTF?
|
|
}
|
|
actionSendFilter.addCategory(Intent.CATEGORY_DEFAULT);
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
actionSendFilter,
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED);
|
|
|
|
// Browser intents are allowed from work profile to parent
|
|
// TODO: Make this configurable, just as ALLOW_PARENT_PROFILE_APP_LINKING in the next function
|
|
IntentFilter browsableIntentFilter = new IntentFilter(Intent.ACTION_VIEW);
|
|
browsableIntentFilter.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
browsableIntentFilter.addDataScheme("http");
|
|
browsableIntentFilter.addDataScheme("https");
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
browsableIntentFilter,
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED);
|
|
IntentFilter browsableDefaultIntentFilter = new IntentFilter(Intent.ACTION_VIEW);
|
|
browsableDefaultIntentFilter.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
browsableDefaultIntentFilter.addCategory(Intent.CATEGORY_DEFAULT);
|
|
browsableDefaultIntentFilter.addDataScheme("http");
|
|
browsableDefaultIntentFilter.addDataScheme("https");
|
|
manager.addCrossProfileIntentFilter(
|
|
adminComponent,
|
|
browsableDefaultIntentFilter,
|
|
DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED);
|
|
|
|
// Block contacts searching optionally
|
|
manager.setCrossProfileContactsSearchDisabled(adminComponent,
|
|
SettingsManager.getInstance().getBlockContactsSearchingEnabled());
|
|
|
|
manager.setProfileEnabled(adminComponent);
|
|
}
|
|
|
|
public static void enforceUserRestrictions(Context context) {
|
|
DevicePolicyManager manager = context.getSystemService(DevicePolicyManager.class);
|
|
ComponentName adminComponent = new ComponentName(context.getApplicationContext(), ShelterDeviceAdminReceiver.class);
|
|
manager.clearUserRestriction(adminComponent, UserManager.DISALLOW_INSTALL_APPS);
|
|
manager.clearUserRestriction(adminComponent, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
|
|
manager.clearUserRestriction(adminComponent, UserManager.DISALLOW_UNINSTALL_APPS);
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
// Polyfill for UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
|
|
// Don't use this on Android Oreo and later, it will crash
|
|
manager.setSecureSetting(adminComponent, Settings.Secure.INSTALL_NON_MARKET_APPS, "1");
|
|
}
|
|
|
|
// TODO: This should be configured by the user, instead of being enforced each time Shelter starts
|
|
// TODO: But we should also have some default restrictions that are set the first time Shelter starts
|
|
manager.addUserRestriction(adminComponent, UserManager.ALLOW_PARENT_PROFILE_APP_LINKING);
|
|
}
|
|
|
|
// Detect if the device is MIUI
|
|
public static boolean isMIUI() {
|
|
try {
|
|
Process proc = Runtime.getRuntime().exec("getprop ro.miui.ui.version.name");
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
|
|
String line = reader.readLine().trim();
|
|
return !line.isEmpty();
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// From <https://stackoverflow.com/questions/3035692/how-to-convert-a-drawable-to-a-bitmap>
|
|
public static Bitmap drawableToBitmap(Drawable drawable) {
|
|
if (drawable instanceof BitmapDrawable) {
|
|
return ((BitmapDrawable)drawable).getBitmap();
|
|
}
|
|
|
|
int width = drawable.getIntrinsicWidth();
|
|
width = width > 0 ? width : 1;
|
|
int height = drawable.getIntrinsicHeight();
|
|
height = height > 0 ? height : 1;
|
|
|
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
Canvas canvas = new Canvas(bitmap);
|
|
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
|
drawable.draw(canvas);
|
|
|
|
return bitmap;
|
|
}
|
|
|
|
public static void killShelterServices(IShelterService serviceMain, IShelterService serviceWork) {
|
|
// Ensure that all our other services are killed at this point
|
|
try {
|
|
serviceWork.stopShelterService(true);
|
|
} catch (Exception e) {
|
|
// We are stopping anyway
|
|
}
|
|
|
|
try {
|
|
serviceMain.stopShelterService(false);
|
|
} catch (Exception e) {
|
|
// We are stopping anyway
|
|
}
|
|
}
|
|
|
|
// Delete apps that no longer exist from the auto freeze list
|
|
public static void deleteMissingApps(String pref, List<ApplicationInfoWrapper> apps) {
|
|
List<String> list = new ArrayList<>(
|
|
Arrays.asList(LocalStorageManager.getInstance().getStringList(pref)));
|
|
list.removeIf((it) -> apps.stream().noneMatch((x) -> x.getPackageName().equals(it)));
|
|
LocalStorageManager.getInstance().setStringList(pref, list.toArray(new String[]{}));
|
|
}
|
|
|
|
public static void createLauncherShortcut(Context context, Intent launchIntent, Icon icon, String id, String label) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
|
|
|
if (shortcutManager.isRequestPinShortcutSupported()) {
|
|
ShortcutInfo info = new ShortcutInfo.Builder(context, id)
|
|
.setIntent(launchIntent)
|
|
.setIcon(icon)
|
|
.setShortLabel(label)
|
|
.setLongLabel(label)
|
|
.build();
|
|
Intent addIntent = shortcutManager.createShortcutResultIntent(info);
|
|
shortcutManager.requestPinShortcut(info,
|
|
PendingIntent.getBroadcast(context, 0, addIntent, 0).getIntentSender());
|
|
} else {
|
|
// TODO: Maybe implement this for launchers without pin shortcut support?
|
|
// TODO: Should be the same with the fallback for Android < O
|
|
// for now just show unsupported
|
|
Toast.makeText(context, context.getString(R.string.unsupported_launcher), Toast.LENGTH_LONG).show();
|
|
}
|
|
} else {
|
|
Intent shortcutIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
|
|
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
|
|
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
|
|
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, drawableToBitmap(icon.loadDrawable(context)));
|
|
context.sendBroadcast(shortcutIntent);
|
|
Toast.makeText(context, R.string.shortcut_create_success, Toast.LENGTH_SHORT).show();
|
|
}
|
|
}
|
|
|
|
public static int getMediaStoreId(Context context, String path) {
|
|
Cursor cursor = context.getContentResolver().query(
|
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
new String[]{MediaStore.MediaColumns._ID},
|
|
MediaStore.MediaColumns.DATA + " LIKE ? ",
|
|
new String[]{path}, null);
|
|
if (cursor == null || cursor.getCount() == 0) {
|
|
return -1;
|
|
} else {
|
|
cursor.moveToFirst();
|
|
return cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
|
|
}
|
|
}
|
|
|
|
// Functions to load scaled down version of Bitmap
|
|
// from <https://developer.android.com/topic/performance/graphics/load-bitmap?hl=es#java>
|
|
public static int calculateInSampleSize(
|
|
BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
|
// Raw height and width of image
|
|
final int height = options.outHeight;
|
|
final int width = options.outWidth;
|
|
int inSampleSize = 1;
|
|
|
|
if (height > reqHeight || width > reqWidth) {
|
|
|
|
final int halfHeight = height / 2;
|
|
final int halfWidth = width / 2;
|
|
|
|
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
|
// height and width larger than the requested height and width.
|
|
while ((halfHeight / inSampleSize) >= reqHeight
|
|
&& (halfWidth / inSampleSize) >= reqWidth) {
|
|
inSampleSize *= 2;
|
|
}
|
|
}
|
|
|
|
return inSampleSize;
|
|
}
|
|
|
|
public static Bitmap decodeSampledBitmap(String filePath,
|
|
int reqWidth, int reqHeight) {
|
|
// First decode with inJustDecodeBounds=true to check dimensions
|
|
final BitmapFactory.Options options = new BitmapFactory.Options();
|
|
options.inJustDecodeBounds = true;
|
|
BitmapFactory.decodeFile(filePath, options);
|
|
|
|
// Calculate inSampleSize
|
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
|
|
|
// Decode bitmap with inSampleSize set
|
|
options.inJustDecodeBounds = false;
|
|
return BitmapFactory.decodeFile(filePath, options);
|
|
}
|
|
|
|
public static Bitmap decodeSampledBitmap(FileDescriptor fd,
|
|
int reqWidth, int reqHeight) {
|
|
// First decode with inJustDecodeBounds=true to check dimensions
|
|
final BitmapFactory.Options options = new BitmapFactory.Options();
|
|
options.inJustDecodeBounds = true;
|
|
BitmapFactory.decodeFileDescriptor(fd, null, options);
|
|
|
|
// Calculate inSampleSize
|
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
|
|
|
// Decode bitmap with inSampleSize set
|
|
options.inJustDecodeBounds = false;
|
|
return BitmapFactory.decodeFileDescriptor(fd, null, options);
|
|
}
|
|
|
|
// Get file's extension name
|
|
public static String getFileExtension(String filePath) {
|
|
int index = filePath.lastIndexOf(".");
|
|
if (index > 0) {
|
|
return filePath.substring(index + 1);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Check if USAGE_STATS is granted
|
|
public static boolean checkUsageStatsPermission(Context context) {
|
|
return checkSpecialAccessPermission(context, AppOpsManager.OPSTR_GET_USAGE_STATS);
|
|
}
|
|
|
|
// Check if SYSTEM_ALERT_WINDOW is granted
|
|
public static boolean checkSystemAlertPermission(Context context) {
|
|
return checkSpecialAccessPermission(context, AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW);
|
|
}
|
|
|
|
// Check if all file access r/w is granted
|
|
@TargetApi(Build.VERSION_CODES.R)
|
|
public static boolean checkAllFileAccessPermission() {
|
|
return Environment.isExternalStorageManager();
|
|
}
|
|
|
|
// Check special access permission through AppOps
|
|
public static boolean checkSpecialAccessPermission(Context context, String name) {
|
|
AppOpsManager appops = context.getSystemService(AppOpsManager.class);
|
|
int mode = appops.checkOpNoThrow(name, android.os.Process.myUid(), context.getPackageName());
|
|
return mode == AppOpsManager.MODE_ALLOWED;
|
|
}
|
|
|
|
// Pipe an InputStream to OutputStream
|
|
public static void pipe(InputStream is, OutputStream os) throws IOException {
|
|
int n;
|
|
byte[] buffer = new byte[65536];
|
|
while ((n = is.read(buffer)) > -1) {
|
|
os.write(buffer, 0, n);
|
|
}
|
|
}
|
|
|
|
// Utilities to build notifications for cross-version compatibility
|
|
private static final String NOTIFICATION_CHANNEL_ID = "ShelterService";
|
|
private static final String NOTIFICATION_CHANNEL_IMPORTANT = "ShelterService-Important";
|
|
public static Notification buildNotification(Context context, String ticker, String title, String desc, int icon) {
|
|
return buildNotification(context, false, ticker, title, desc, icon);
|
|
}
|
|
|
|
public static Notification buildNotification(Context context, boolean important, String ticker, String title, String desc, int icon) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
return buildNotificationOreo(context, important, ticker, title, desc, icon);
|
|
} else {
|
|
return buildNotificationLollipop(context, important, ticker, title, desc, icon);
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
private static Notification buildNotificationLollipop(Context context, boolean important, String ticker, String title, String desc, int icon) {
|
|
return new Notification.Builder(context)
|
|
.setTicker(ticker)
|
|
.setContentTitle(title)
|
|
.setContentText(desc)
|
|
.setSmallIcon(icon)
|
|
.setPriority(important ? Notification.PRIORITY_MAX : Notification.PRIORITY_MIN)
|
|
.build();
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
|
private static Notification buildNotificationOreo(Context context, boolean important, String ticker, String title, String desc, int icon) {
|
|
String id = important ? NOTIFICATION_CHANNEL_IMPORTANT : NOTIFICATION_CHANNEL_ID;
|
|
// Android O and later: Notification Channel
|
|
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
|
if (nm.getNotificationChannel(id) == null) {
|
|
NotificationChannel chan = new NotificationChannel(
|
|
id,
|
|
important ? context.getString(R.string.notifications_important)
|
|
: context.getString(R.string.app_name),
|
|
important ? NotificationManager.IMPORTANCE_HIGH
|
|
: NotificationManager.IMPORTANCE_MIN);
|
|
nm.createNotificationChannel(chan);
|
|
}
|
|
|
|
// Disable everything: do not disturb the user
|
|
NotificationChannel chan = nm.getNotificationChannel(id);
|
|
if (!important) {
|
|
chan.enableVibration(false);
|
|
chan.enableLights(false);
|
|
chan.setImportance(NotificationManager.IMPORTANCE_MIN);
|
|
} else {
|
|
chan.enableVibration(true);
|
|
chan.setImportance(NotificationManager.IMPORTANCE_HIGH);
|
|
}
|
|
nm.createNotificationChannel(chan);
|
|
|
|
// Create foreground notification to keep the service alive
|
|
return new Notification.Builder(context, id)
|
|
.setTicker(ticker)
|
|
.setContentTitle(title)
|
|
.setContentText(desc)
|
|
.setSmallIcon(icon)
|
|
.build();
|
|
}
|
|
|
|
// A wrapper over arbitrary ActivityResultContract that provides
|
|
// hardcoded input parameters and do not accept input with launch()
|
|
public static class ActivityResultContractInputWrapper<I, O, T extends ActivityResultContract<I, O>>
|
|
extends ActivityResultContract<Void, O> {
|
|
private final T mInner;
|
|
private final I mInput;
|
|
|
|
public ActivityResultContractInputWrapper(T inner, I input) {
|
|
mInner = inner;
|
|
mInput = input;
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public Intent createIntent(@NonNull Context context, Void input) {
|
|
return mInner.createIntent(context, mInput);
|
|
}
|
|
|
|
@Override
|
|
public O parseResult(int resultCode, @Nullable Intent intent) {
|
|
return mInner.parseResult(resultCode, intent);
|
|
}
|
|
}
|
|
}
|