package com.njlabs.showjava.processor;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;
import com.njlabs.showjava.Constants;
import com.njlabs.showjava.R;
import com.njlabs.showjava.ui.AppProcessActivity;
import com.njlabs.showjava.ui.JavaExplorer;
import com.njlabs.showjava.utils.ExceptionHandler;
import com.njlabs.showjava.utils.Notify;
import com.njlabs.showjava.utils.SourceInfo;
import com.njlabs.showjava.utils.Utils;
import com.njlabs.showjava.utils.logging.Ln;
import net.dongliu.apk.parser.ApkParser;
import java.io.File;
@SuppressWarnings({"ConstantConditions", "ResultOfMethodCallIgnored"})
public class ProcessService extends Service {
public String packageName;
public String packageFilePath;
public String packageLabel;
public String javaSourceOutputDir;
public String sourceOutputDir;
public ExceptionHandler exceptionHandler;
public Handler UIHandler;
private Notify processNotify;
public ApkParser apkParser;
public String decompilerToUse = "cfr";
private int startID;
public void onCreate() {
super.onCreate();
}
public int STACK_SIZE;
public boolean IGNORE_LIBS;
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
this.startID = startId;
/**
* Initialize a handler for posting runnables that have to run on the UI thread
*/
UIHandler = new Handler();
/**
* Receive action from the intent and decide whether to start or stop the existing process
*/
if (intent.getAction().equals(Constants.ACTION.START_PROCESS)) {
/**
* The intent's actions is {@link Constants.ACTION.START_PROCESS}
* Which means, the process has to start.
*
* We build the notification and start the process as a foreground process (to prevent it
* from being killed on exit)
*/
startForeground(Constants.PROCESS_NOTIFICATION_ID, buildNotification());
handleIntent(intent);
} else if (intent.getAction().equals(Constants.ACTION.STOP_PROCESS)) {
/**
* The intent's actions is {@link Constants.ACTION.STOP_PROCESS}
* Which means, the process has to stop and kill itself.
*
* We are broadcasting an 'exit' status so that any activity listening can exit.
* We stop the foreground process.
* And we forcefully kill the service.
*
* Uses the {@link #killSelf()} method.
*/
int toKillStartId = intent.getIntExtra("startId",-1);
killSelf(true, toKillStartId);
} else if(intent.getAction().equals(Constants.ACTION.STOP_PROCESS_FOR_NEW)) {
killSelf(false, -1);
}
return START_NOT_STICKY;
}
private void handleIntent(Intent workIntent) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
STACK_SIZE = Integer.valueOf(prefs.getString("thread_stack_size", String.valueOf(20 * 1024 * 1024)));
IGNORE_LIBS = prefs.getBoolean("ignore_libraries", true);
/**
* This is the main starting point of the ProcessorService. The intent is read and handled here
*/
Bundle extras = workIntent.getExtras();
if (extras != null) {
if(extras.containsKey("decompiler")){
decompilerToUse = extras.getString("decompiler");
}
packageFilePath = extras.getString("package_file_path");
(new Thread(new Runnable() {
@Override
public void run() {
try {
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(packageFilePath, 0);
apkParser = new ApkParser(new File(packageFilePath));
packageLabel = apkParser.getApkMeta().getLabel();
packageName = packageInfo.packageName;
} catch (Exception e) {
Ln.e(e);
broadcastStatus("exit_process_on_error");
}
UIHandler.post(new Runnable() {
@Override
public void run() {
Intent resultIntent = new Intent(getApplicationContext(), AppProcessActivity.class);
resultIntent.putExtra("from_notification", true);
resultIntent.putExtra("package_name", packageName);
resultIntent.putExtra("package_label", packageLabel);
resultIntent.putExtra("package_file_path", packageFilePath);
resultIntent.putExtra("decompiler", decompilerToUse);
PendingIntent resultPendingIntent =
PendingIntent.getActivity(ProcessService.this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);
processNotify.updateIntent(resultPendingIntent);
}
});
sourceOutputDir = Environment.getExternalStorageDirectory() + "/ShowJava/sources/" + packageName;
javaSourceOutputDir = sourceOutputDir + "/java";
Ln.d(sourceOutputDir);
SourceInfo.initialise(ProcessService.this);
UIHandler.post(new Runnable() {
@Override
public void run() {
exceptionHandler = new ExceptionHandler(getApplicationContext(), javaSourceOutputDir, packageName);
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
Processor.extract(ProcessService.this);
}
});
}
})).start();
} else {
killSelf(true, startID);
}
}
public void publishProgress(String progressText) {
switch (progressText) {
case "start_activity": {
decompileDone();
broadcastStatusWithPackageInfo(progressText, sourceOutputDir, packageName);
kill();
break;
}
case "start_activity_with_error": {
decompileDone();
broadcastStatusWithPackageInfo(progressText, sourceOutputDir, packageName);
UIHandler.post(new ToastRunnable("Decompilation completed with errors. This incident has been reported to the developer."));
kill();
break;
}
case "exit_process_on_error":
broadcastStatus(progressText);
UIHandler.post(new ToastRunnable("The app you selected cannot be decompiled. Please select another app."));
kill();
break;
default:
break;
}
}
private void decompileDone() {
showCompletedNotification();
}
private void showCompletedNotification() {
Intent resultIntent = new Intent(getApplicationContext(), JavaExplorer.class);
resultIntent.putExtra("from_notification", true);
resultIntent.putExtra("java_source_dir", sourceOutputDir);
resultIntent.putExtra("package_id", packageName);
PendingIntent resultPendingIntent =
PendingIntent.getActivity(ProcessService.this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle(packageLabel + " has been decompiled.")
.setContentText("Tap to browse source")
.setSmallIcon(R.drawable.stat_action_done)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(resultPendingIntent)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setAutoCancel(true);
mNotifyManager.notify(2, mBuilder.build());
}
public void broadcastStatus(String status) {
sendNotification(status, "");
Intent localIntent = new Intent(Constants.PROCESS_BROADCAST_ACTION)
.putExtra(Constants.PROCESS_STATUS_KEY, status);
sendBroadcast(localIntent);
}
public void broadcastStatus(String statusKey, String statusData) {
sendNotification(statusKey, statusData);
Intent localIntent = new Intent(Constants.PROCESS_BROADCAST_ACTION)
.putExtra(Constants.PROCESS_STATUS_KEY, statusKey)
.putExtra(Constants.PROCESS_STATUS_MESSAGE, statusData);
sendBroadcast(localIntent);
}
public void broadcastStatusWithPackageInfo(String statusKey, String dir, String packId) {
sendNotification(statusKey, "");
Intent localIntent = new Intent(Constants.PROCESS_BROADCAST_ACTION)
.putExtra(Constants.PROCESS_STATUS_KEY, statusKey)
.putExtra(Constants.PROCESS_DIR, dir)
.putExtra(Constants.PROCESS_PACKAGE_ID, packId);
sendBroadcast(localIntent);
}
private void sendNotification(String statusKey, String statusData) {
switch (statusKey) {
case "optimise_dex_start":
processNotify.updateTitleText("Optimising dex file", "Processing ...");
break;
case "optimising":
processNotify.updateTitleText("Optimising dex file", "Processing ...");
break;
case "optimise_dex_finish":
processNotify.updateTitleText("Finishing optimisation", "Processing ...");
break;
case "merging_classes":
processNotify.updateTitleText("Merging classes", "Processing ...");
break;
case "start_activity":
processNotify.cancel();
break;
case "start_activity_with_error":
processNotify.cancel();
break;
case "exit_process_on_error":
processNotify.cancel();
break;
case "finaldex":
processNotify.updateTitleText("Finishing optimisation", "Processing ...");
break;
case "dex2jar":
processNotify.updateTitleText("Decompiling dex to jar", "Processing ...");
break;
case "jar2java":
processNotify.updateTitleText("Decompiling to java", "Processing ...");
break;
case "res":
processNotify.updateTitleText("Extracting Resources", "Processing ...");
break;
case "exit":
try {
processNotify.cancel();
} catch (Exception e) {
Ln.i(e);
}
break;
default:
processNotify.updateText(statusData);
}
}
private Notification buildNotification() {
Intent stopIntent = new Intent(this, ProcessService.class);
stopIntent.setAction(Constants.ACTION.STOP_PROCESS);
stopIntent.putExtra("startID", startID);
PendingIntent pendingStopIntent = PendingIntent.getService(this, 0, stopIntent, 0);
NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle("Decompiling")
.setContentText("Processing the apk")
.setSmallIcon(R.drawable.stat_action_running)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setOngoing(true)
.addAction(R.drawable.ic_action_kill, "Stop decompiler", pendingStopIntent)
.setAutoCancel(false);
mBuilder.setProgress(0, 0, true);
Notification notification = mBuilder.build();
mNotifyManager.notify(Constants.PROCESS_NOTIFICATION_ID, notification);
processNotify = new Notify(mNotifyManager, mBuilder, Constants.PROCESS_NOTIFICATION_ID);
return notification;
}
@Override
public void onDestroy() {
super.onDestroy();
try {
processNotify.cancel();
} catch (Exception e) {
Ln.e(e);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void kill() {
stopForeground(true);
stopSelf();
}
private class ToastRunnable implements Runnable {
final String mText;
public ToastRunnable(String text) {
mText = text;
}
@Override
public void run() {
Toast.makeText(getApplicationContext(), mText, Toast.LENGTH_SHORT).show();
}
}
private void killSelf(boolean shouldBroadcast, int startID){
if(shouldBroadcast){
broadcastStatus("exit");
}
stopForeground(true);
stopSelf(startID);
try {
NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotifyManager.cancel(Constants.PROCESS_NOTIFICATION_ID);
Utils.forceKillAllProcessorServices(this);
} catch (Exception e) {
Ln.e(e);
}
}
}