Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) {
if (enabled) {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build();

PeriodicWorkRequest saveRequest = new PeriodicWorkRequest.Builder(HostsDownloadWorker.class, 24,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
Expand Down Expand Up @@ -74,14 +75,35 @@ public static synchronized TrackerAnalysisManager getInstance(Context context) {
* @param packageName The package to analyze
*/
public void startAnalysis(String packageName) {
startAnalysis(packageName, false);
}

/**
* Starts an analysis for the given package using WorkManager.
* Only one analysis runs at a time to prevent OOM; others are queued.
* Observe progress via {@link #getWorkInfoByPackageLiveData(String)}.
*
* @param packageName The package to analyze
* @param allowOnBattery If true, analysis can run on battery; if false, requires charging
*/
public void startAnalysis(String packageName, boolean allowOnBattery) {
Data inputData = new Data.Builder()
.putString(TrackerAnalysisWorker.KEY_PACKAGE_NAME, packageName)
.build();

OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(TrackerAnalysisWorker.class)
OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(TrackerAnalysisWorker.class)
.setInputData(inputData)
.addTag(packageName)
.build();
.addTag(packageName);

// Add constraint to only run when device is charging to save battery (unless explicitly allowed on battery)
if (!allowOnBattery) {
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(true)
.build();
workRequestBuilder.setConstraints(constraints);
}

OneTimeWorkRequest workRequest = workRequestBuilder.build();

// Use global work name + APPEND to serialize all analyses (prevents OOM from
// concurrent scans)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,48 @@

package net.kollnig.missioncontrol.analysis;

import static net.kollnig.missioncontrol.DetailsActivity.INTENT_EXTRA_APP_NAME;
import static net.kollnig.missioncontrol.DetailsActivity.INTENT_EXTRA_APP_PACKAGENAME;
import static net.kollnig.missioncontrol.DetailsActivity.INTENT_EXTRA_APP_UID;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import net.kollnig.missioncontrol.DetailsActivity;
import net.kollnig.missioncontrol.R;

import org.apache.commons.lang3.StringUtils;

import java.util.concurrent.atomic.AtomicInteger;

import eu.faircode.netguard.PendingIntentCompat;
import eu.faircode.netguard.ServiceSinkhole;

public class TrackerAnalysisWorker extends Worker {
private static final String TAG = "TrackerControl.Analysis";

public static final String KEY_PACKAGE_NAME = "package_name";
public static final String KEY_RESULT = "result";
public static final String KEY_ERROR = "error";
public static final String KEY_PROGRESS = "progress";

// Base notification ID for analysis completion notifications
// Use package hash offset to avoid conflicts with other notification IDs
private static final int NOTIFICATION_ID_BASE = 20000;

public TrackerAnalysisWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
Expand All @@ -59,6 +85,12 @@ public Result doWork() {
TrackerAnalysisManager.getInstance(context)
.cacheResult(packageName, result, pkg.versionCode);

// Show notification when analysis is finished successfully
// Only show if result is not null (analysis completed successfully)
if (result != null) {
showCompletionNotification(context, packageName, result, pkg);
}

return Result.success(new Data.Builder()
.putString(KEY_RESULT, result)
.build());
Expand Down Expand Up @@ -91,4 +123,55 @@ private String doAnalysis(Context context, String packageName) throws AnalysisEx
});
return analyser.analyseApp(packageName);
}

private void showCompletionNotification(Context context, String packageName, String result, PackageInfo pkg) {
try {
// Create notification channel for Android O+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel("analysis",
context.getString(R.string.static_analysis),
NotificationManager.IMPORTANCE_DEFAULT);
nm.createNotificationChannel(channel);
}

// Get app name
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
String appName = pm.getApplicationLabel(appInfo).toString();

// Count trackers found
// Note: This uses the same bullet character counting method as ServiceSinkhole
// for consistency with the existing notification system
int trackerCount = StringUtils.countMatches(result, "•");

// Build notification
Intent main = new Intent(context, DetailsActivity.class);
main.putExtra(INTENT_EXTRA_APP_NAME, appName);
main.putExtra(INTENT_EXTRA_APP_PACKAGENAME, packageName);
main.putExtra(INTENT_EXTRA_APP_UID, pkg.applicationInfo.uid);
PendingIntent pi = PendingIntentCompat.getActivity(context, 0, main, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "analysis")
.setSmallIcon(R.drawable.ic_rocket_white)
.setContentTitle(context.getString(R.string.static_analysis))
.setContentText(context.getString(R.string.msg_installed_tracker_libraries_found, trackerCount))
.setContentIntent(pi)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);

// Show notification with unique ID based on package name
// Using hashCode may have collisions but is acceptable since:
// 1. Package names are unique per device
// 2. Collision only affects notification grouping, not functionality
// 3. Adding base offset reduces collision risk with other notification IDs
int notificationId = NOTIFICATION_ID_BASE + Math.abs(packageName.hashCode());
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(notificationId, builder.build());

} catch (Exception e) {
// Don't fail the worker if notification fails
Log.e(TAG, "Failed to show completion notification: " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ private void setupTrackerAnalysisButton(View view) {
}

mBtnAnalyze.setOnClickListener(v -> {
manager.startAnalysis(mAppId);
// Allow manual analysis to run on battery
manager.startAnalysis(mAppId, true);
// Fragment's observer will pick up the work state changes
});
}
Expand Down