diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..9af7c01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +**/build +.idea +/captures +app/out* +/keystores/release.properties diff --git a/README.md b/README.md new file mode 100755 index 0000000..ce3e936 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Candy Crush Solver + +This is an Android project that solves the game Candy Crush. +This service scans the grid to find sweets, find the best move for the player and displays it on the screen, on top of Candy Crush. For more information, read here: https://applidium.com/news/candy_crush_solver/. + +## Setup + +This project uses the Android version of OpenCV libraries. +If our setup doesn't work on your computer, we advise you to follow this [tutorial][]. + +In order to run the tests, you might want to add `-Djava.library.path=src/test` to your VM options if you want to run your tests outside of gradle. +(For VM options in Android Studio, follow this path : Run -> Edit Configuration -> JUnit -> VM Options.) + +Finally, if you want to use analytics or crash reporting, everything is ready in the code : just add an HockeyApp key to `/keystore/release.properties` (variable `keyAlias`). To get your own key, go to [HockeyApp website][] and create a new project. + +[tutorial]: https://www.youtube.com/watch?v=OTw_GIQNbD8 +[HockeyApp website]: https://hockeyapp.net + +## Project architecture + +### Launch + +`MainActivity` starts when the user launches the app, with its `SettingsFragment`. Several permissions are needed before beginning : overlay permission, accessibility access. Another button is here to start taking screenshots. When these three permissions are unlocked, our `HeadService starts`. + +Besides, `TutoActivity`, and other tuto fragments compose a little tutorial. +The `PermissionChecker` helps to know when overlay permission is on. + +### Screenshot + +Most of the code lays in the same named class. Once started in `SettingsFragment`, a new capture is saved each time the `ImageReader.OnImageAvailableListener` detects a change in the screen. The image is saved in the internal storage of the phone. + +### Accessibility + +The `HeadService` checks if the user is on Candy Crush or not. It won't start any action before that. We also pay attention that screenshots are actually taken (by checking its modification time). Moreover, we stop our service when the user quits Candy Crush. + +```java +if (event.getPackageName().toString().equals("com.king.candycrushsaga") && Math.abs(lastModDate.getTime() - d.getTime()) < TIME_LIMIT) { + launchBusinessService(); + } else { + stopBusinessService(); + } +} +``` + +Then the app launches the `BusinessService` at regular intervals. + +### Grid recognition + +The `BusinessService calls the `FeaturesExtractor` and let the engine work. Several methods can be used here : + +1. Call `private List extractSweetsForFeature(Mat img, int feature, int i, int orientation)` if you prefer a quick recognition. This function uses the pixel colors to find Candy Crush sweets. The integer `feature` represents its color code. But this methods won't be efficient for a black and white pattern. We chose this one in our solver, because the the algorithm performances are far better. +(Note : this recognition is not as precise as the next one, so don't forget to center your final display) + +2. Call `public List extractSweetsForFeatureWithOpenCV(Mat img, Mat feature, int i)` if you prefer a really precise recognition. However, it is really slower (but depends of the size of your screenshot). + +The engine also contains a `FeaturePainter` that can help you see the results of this grid recognition. + +### Find a move + +Then the class `MoveFinder` works to find every move on the screen, and gives at the end the best one. The numerous booleans present aim to find special moves (will 4 sweets be aligned ? Or 5 ? Or can I make a sweet bomb with this move ?). If the algorithm seems a bit confusing, find explanation on our [blogpost][]. + +[blogpost]: https://www.youtube.com/watch?v=OTw_GIQNbD8 + +### Solution display + +Finally, the object HeadLayer represents the overlay on which we can draw. Here is the global setup : + +```java +private WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSLUCENT); +``` + +And then you can add whatever you wan on the layout : + +```java +private void addToWindowManager() { + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + windowManager.addView(frameLayout, params); + + LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + layoutInflater.inflate(R.layout.head, frameLayout); + image = (ImageView) frameLayout.findViewById(R.id.solution); + } +} +``` + +And don't forget to add this view in your `head.xml` file. +In our case, we chose to display the solution as a black transparent bitmap, with a white hole on the move we want to show. + +## Make your own game solver ! + +This project aims to be as general as possible, in order to be reused for other logic game resolution. We encourage you to create your own solver, based on this project. For example, feel free to develop a sudoku solver ! + +## Known bugs + +We are still working to fix the following bugs : + +1. Orange sweets are mixed up with yellow ones on some devices, which leads to displaying wrong moves. + +2. There is an offset on solution display on Samsung Galaxy S6. + +Don't hesitate to contact us if you have any clues or if you find other errors. diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 index 0000000..c6cbe56 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 0000000..c2de8e7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,204 @@ +apply plugin: 'android-sdk-manager' +apply plugin: 'com.android.application' +apply plugin: 'com.neenbedankt.android-apt' +apply plugin: 'com.tmiyamon.config' +apply plugin: 'me.leolin.gradle-android-aspectj' + +android { + compileSdkVersion 24 + buildToolsVersion "23.0.3" + + defaultConfig { + applicationId "com.applidium.candycrushsolver" + //the class "android.media.projection.MediaProjectionManager" doesn't exist on API lower than 21 and is compulsory for our screenshots + minSdkVersion 21 + targetSdkVersion 24 + versionCode getVersionCode(1) + versionName getVersionName("0.1") + } + + productFlavors { + stubs + preprod + prod + } + + variantFilter { variant -> associateProdWithRelease(variant) } + + signingConfigs { + release { + Properties props = new Properties() + props.load(new FileInputStream(file("../keystores/release.properties"))) + storeFile file(props['storeFile']) + storePassword props['storePassword'] + keyAlias props['keyAlias'] + keyPassword props['keyPassword'] + } + } + + buildTypes { + distrib.initWith(buildTypes.debug) + release.setSigningConfig(signingConfigs.release) + + buildTypes.each { + Properties props = new Properties() + props.load(new FileInputStream(file("../keystores/release.properties"))) + it.buildConfigField 'String', 'APP_KEY', props['keyAlias'] as String + } + } + + decorateFlavors() + + + sourceSets.main { + jniLibs.srcDir '../libs/opencv/jniLibs' + } + + testOptions { + unitTests.all { + jvmArgs '-Djava.library.path=src/test' + } + } + + packagingOptions { + exclude 'META-INF/maven/commons-io/commons-io/pom.xml' + exclude 'META-INF/maven/commons-io/commons-io/pom.properties' + } + + // Open finder on the apks when doing release builds + applicationVariants.all { variant -> + variant.assemble.doLast { + //If this is a 'release' build, reveal the compiled apk in finder/explorer + if (variant.buildType.name.contains('release')) { + + def path = null; + variant.outputs.each { output -> + path = output.outputFile + } + + exec { + if (System.properties['os.name'].toLowerCase().contains('mac os x')) { + ['open', '-R', path].execute() + } else if (System.properties['os.name'].toLowerCase().contains('windows')) { + ['explorer', '/select,', path].execute() + } + } + } + } + } +} + +repositories { + flatDir { + dirs "libs" + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:palette-v7:24.1.1' + compile 'org.apache.commons:commons-io:1.3.2' + compile project(':libs:opencv') + compile 'com.viewpagerindicator:library:2.4.1@aar' + compile 'net.hockeyapp.android:HockeySDK:4.1.0-beta.2' + + testCompile 'junit:junit:4.12' + + compile "com.google.dagger:dagger:2.5" + apt "com.google.dagger:dagger-compiler:2.5" + provided "javax.annotation:javax.annotation-api:1.2" +} + +task copyResDirectoryToClasses(type: Copy){ + from "${projectDir}/src/main/res/raw" + into "${buildDir}/intermediates/classes/test/debug/assets" + from "${projectDir}/src/test/res" + into "${buildDir}/intermediates/classes/test/debug/assets" +} + +afterEvaluate { + preBuild.dependsOn(copyResDirectoryToClasses) +} + +private decorateFlavors() { + android.buildTypes.all { type -> + + if (!type.debuggable) { + return + } + + def version = android.defaultConfig.versionCode + def sha1 = 'git rev-parse --short HEAD'.execute().text.trim() + + applicationIdSuffix = ".${type.name}" + versionNameSuffix = "-${version}-${sha1}-${type.name}" + } + android.productFlavors.all { flavor -> + def String name = flavor.name + + if ("prod".equals(name)) { + return + } + + applicationId = appendedApplicationId(name) + versionName = appendedVersionName(name) + } +} + +private String appendedVersionName(String name) { + android.defaultConfig.versionName + toAppend(name, "-") +} + +private String appendedApplicationId(String name) { + android.defaultConfig.applicationId + toAppend(name, ".") +} + +private String toAppend(String name, String sep) { + name.isEmpty() ? "" : sep + name +} + +Closure associateProdWithRelease(variant) { + def isRelease = variant.buildType.name.equals('release') + def isProd = variant.getFlavors().get(0).name.equals('prod') + + if (isRelease) { + variant.setIgnore(!isProd); + } +} + +def getVersionName(defaultVName) { + if (project.hasProperty('vName') && project.vName) { + return project.vName + } + return defaultVName +} + +def getVersionCode(defaultVCode) { + if (project.hasProperty('vCode') && project.vCode) { + return project.vCode.toInteger() + } + return defaultVCode +} + + +task('getVariantInfo') << { + println "Variants:\n" + android.applicationVariants.all { v -> + println v.name + println v.buildType.name + println v.buildType.applicationIdSuffix + println v.buildType.versionNameSuffix + println v.mergedFlavor.versionName + println v.mergedFlavor.applicationId + println "" + } +} + +def installAll = tasks.create('installAll') +installAll.description = 'Install all applications.' +android.applicationVariants.all { variant -> + installAll.dependsOn(variant.install) + // Ensure we end up in the same group as the other install tasks. + installAll.group = variant.install.group +} diff --git a/app/config/debug.yml b/app/config/debug.yml new file mode 100644 index 0000000..1986dfa --- /dev/null +++ b/app/config/debug.yml @@ -0,0 +1,11 @@ +logging: + enabled: true + show_hashs: false +performance: + leak_canary: true + dev_metrics: true + stetho: true +errors: + rethrow: false +crashes: + enabled: false diff --git a/app/config/default.yml b/app/config/default.yml new file mode 100644 index 0000000..c9aa401 --- /dev/null +++ b/app/config/default.yml @@ -0,0 +1,17 @@ +logging: + enabled: false + show_hashs: false +tracing: + enabled: true +performance: + leak_canary: false + dev_metrics: false + stetho: false +errors: + rethrow: true +crashes: + enabled: true + additional_data: false + max_network: 10 + max_trace: 10 + max_component: 10 diff --git a/app/config/distrib.yml b/app/config/distrib.yml new file mode 100644 index 0000000..9f07020 --- /dev/null +++ b/app/config/distrib.yml @@ -0,0 +1,15 @@ +logging: + enabled: false + show_hashs: false +performance: + leak_canary: false + dev_metrics: false + stetho: false +errors: + rethrow: false +crashes: + enabled: true + additional_data: true + max_network: 100 + max_trace: 100 + max_component: 100 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..cf9532c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/BusinessService.java b/app/src/main/java/com/applidium/candycrushsolver/android/BusinessService.java new file mode 100644 index 0000000..9909eec --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/BusinessService.java @@ -0,0 +1,284 @@ +package com.applidium.candycrushsolver.android; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; + +import com.applidium.candycrushsolver.R; +import com.applidium.candycrushsolver.engine.FeaturesExtractor; +import com.applidium.candycrushsolver.engine.Move; +import com.applidium.candycrushsolver.engine.MoveFinder; +import com.applidium.candycrushsolver.engine.Sweet; + +import org.opencv.android.OpenCVLoader; +import org.opencv.android.Utils; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import timber.log.Timber; + +public class BusinessService extends Service { + + private static final int NB_FEATURES = 6; + private static final int SWEET_SIZE = 45; + private static final int FEATURE_SIZE = 35; + private String STORE_DIRECTORY; + private int DELAY = 1000; + private static final String KEY_STRING_CHOICE = "KEY_STRING_CHOICE"; + + private HeadLayer headLayer; + private boolean choseBestMove; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { //your attention space rangers, this is starCommand ! + Timber.v("Life cycle : service just started"); + initHeadLayer(); + STORE_DIRECTORY = getFilesDir().getAbsolutePath(); + choseBestMove = intent.getExtras().getBoolean(KEY_STRING_CHOICE); + Timber.v("The choice is %s", choseBestMove); + findAndDisplaySolution(); + + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + destroyHeadLayer(); + stopForeground(true); + } + + private void initHeadLayer() { + Timber.v("App cycle : service inside initialisation"); + headLayer = new HeadLayer(this); + } + + private void destroyHeadLayer() { + if (headLayer == null) { + return; + } + headLayer.destroy(); + headLayer = null; + } + + private void findAndDisplaySolution() { + Timber.v("App cycle : begin work"); + float density = getResources().getDisplayMetrics().density; + int[] features = loadFeatures(); + Mat image = Imgcodecs.imread(STORE_DIRECTORY + "/myscreen.png"); + if (image == null || image.empty()) { + Toast.makeText(this, getResources().getString(R.string.err_image), Toast.LENGTH_LONG).show(); + return; + } + + FeaturesExtractor extractor = new FeaturesExtractor(); + int orientation = getResources().getConfiguration().orientation; + Timber.v("App cycle : just before extractor"); + List> grid = extractor.extractFeaturesFromImage(image, features, orientation); + Timber.v("App cycle : just after extractor"); + + if (grid == null) { + Toast.makeText(this, getResources().getString(R.string.err_grid), Toast.LENGTH_LONG).show(); + return; + } + + int [] numberOfColorsInGrid = getNumberOfColorsInGrid(grid); + int percentageOfOrangeSweets = numberOfColorsInGrid[2] / 70; //2 is the number for orange color and 70 is approximately a grid size + + int totalNumberOfSweets = + numberOfColorsInGrid[0] + + numberOfColorsInGrid[1] + + numberOfColorsInGrid[2] + + numberOfColorsInGrid[3] + + numberOfColorsInGrid[4] + + numberOfColorsInGrid[5]; + //try to find if the screen is a level or not + if (percentageOfOrangeSweets > 0.5 || getNumberOfDifferentColorsInGrid(grid) < 3) { + //we don't show result for grid with too much orange sweets + //because it often means that we're on the splash screen + //(or when there are not a lot of colors - meaning we're on a level selection or menu screen) + // -- no error message on a toast because the player doesn't care yet what our services does (as he is not on the level) + return; + } + + if (totalNumberOfSweets < 30) { + //we don't show results for grid with less than 20 sweets : + //because it often means that we're not on the level yet (level selection for example) + //or the screenshot is not perfectly loaded and solution will be flawed + Toast.makeText(this, getResources().getString(R.string.err_grid), Toast.LENGTH_LONG).show(); + return; + } + + MoveFinder mv = new MoveFinder(grid); + if (choseBestMove) { + Timber.v("answer yes"); + findAndDisplayBestMove(density, image, mv); + } else { + Timber.v("answer no"); + findAndDisplayEveryMove(density, image, mv); + } + } + + private void findAndDisplayBestMove(float density, Mat resizeImage, MoveFinder mv) { + Move best = mv.findMove(); + if (best == null) { + Toast.makeText(this, getResources().getString(R.string.err_move), Toast.LENGTH_SHORT).show(); + return; + } + try { + Mat[] featuresOpenCV = loadFeaturesForOpenCvVersion(); + recenterSweet(best, resizeImage, featuresOpenCV); + Timber.v("App cycle : move just found"); + headLayer.showBestMoveOnScreen(best, density); + Timber.v("App cycle : move on screen"); + clearScreenAfterAMoment(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void findAndDisplayEveryMove(float density, Mat resizeImage, MoveFinder mv) { + mv.findAllMoves(); + List moves = mv.getMoves(); + if (moves == null || moves.size() == 0) { + Toast.makeText(this, getResources().getString(R.string.err_move), Toast.LENGTH_SHORT).show(); + return; + } + try { + Mat[] featuresOpenCV = loadFeaturesForOpenCvVersion(); + for (Move move : moves) { + recenterSweet(move, resizeImage, featuresOpenCV); + } + Timber.v("App cycle : moves just found"); + headLayer.showMovesOnScreen(moves, density); + Timber.v("App cycle : moves on screen"); + DELAY = 6000; + clearScreenAfterAMoment(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void clearScreenAfterAMoment() { + new Handler().postDelayed(new Runnable() { + public void run() { + Timber.v("App cycle : time to put display off"); + if (headLayer != null) { + headLayer.destroy(); + } + } + }, DELAY); + } + + private int [] getNumberOfColorsInGrid(List> grid) { + int [] colorsPresent = new int [6]; + for (int i = 0; i < grid.size(); i++) { + if (grid.get(i) != null) { + for (int j = 0; j < grid.get(i).size(); j++) { + if (grid.get(i).get(j) != null) { + colorsPresent[grid.get(i).get(j).getType().ordinal()] += 1; + } + } + } + } + Timber.v("Color counts are : %s", Arrays.toString(colorsPresent)); + return colorsPresent; + } + + private int getNumberOfDifferentColorsInGrid(List> grid) { + int [] colorsPresent = new int [6]; + for (int i = 0; i < grid.size(); i++) { + if (grid.get(i) != null) { + for (int j = 0; j < grid.get(i).size(); j++) { + if (grid.get(i).get(j) != null) { + colorsPresent[grid.get(i).get(j).getType().ordinal()] = 1; + } + } + } + } + int sum = 0; + for( int i : colorsPresent) { + sum += i; + } + return sum; + } + + public void recenterSweet(Move best, Mat image, Mat[] features) { + int x = (int) best.getSweet1().getX(); + int y = (int) best.getSweet1().getY(); + + Mat cropped = cropImage(image, x, y); + Mat result = cropped.clone(); + Mat feature = resizeFeature(best, features); + + Imgproc.matchTemplate(cropped, feature, result, Imgproc.TM_CCOEFF_NORMED); + Core.MinMaxLocResult mmr = Core.minMaxLoc(result); + Point matchLoc = mmr.maxLoc; + int xInCroppedImage = (int) matchLoc.x; + int yInCroppedImage = (int) matchLoc.y; + + //convert for big image + int refX = Math.min(xInCroppedImage + Math.max(0, x - SWEET_SIZE), image.cols()); + int refY = Math.min(yInCroppedImage + Math.max(0, y - SWEET_SIZE), image.rows()); + + best.getSweet1().setPosition(new Point(refX, refY)); + } + + private Mat resizeFeature(Move best, Mat[] features) { + int color = best.getSweet1().getType().ordinal(); + Size sz = new Size(FEATURE_SIZE, FEATURE_SIZE); + Imgproc.resize(features[color], features[color], sz); + return features[color]; + } + + private Mat cropImage(Mat image, int x, int y) { + return image.submat( + Math.max(0, y - SWEET_SIZE), + Math.min(image.rows(), y + SWEET_SIZE), + Math.max(0, x - SWEET_SIZE), + Math.min(image.cols(), x + SWEET_SIZE) + ); + } + + protected int[] loadFeatures() { + return new int[]{ + ContextCompat.getColor(this, R.color.colorGreen), + ContextCompat.getColor(this, R.color.colorRed), + ContextCompat.getColor(this, R.color.colorOrange), + ContextCompat.getColor(this, R.color.colorYellow), + ContextCompat.getColor(this, R.color.colorPurple), + ContextCompat.getColor(this, R.color.colorBlue) + }; + } + + protected Mat[] loadFeaturesForOpenCvVersion() throws IOException { + Mat[] features = new Mat[NB_FEATURES]; + if (!OpenCVLoader.initDebug()) { + Toast.makeText(this, getResources().getString(R.string.err_opencv), Toast.LENGTH_LONG).show(); + } else { + features[0] = Utils.loadResource(this, R.raw.green); + features[1] = Utils.loadResource(this, R.raw.red); + features[2] = Utils.loadResource(this, R.raw.orange); + features[3] = Utils.loadResource(this, R.raw.yellow); + features[4] = Utils.loadResource(this, R.raw.purple); + features[5] = Utils.loadResource(this, R.raw.blue); + } + return features; + } + + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/CandyCrushSolverApplication.java b/app/src/main/java/com/applidium/candycrushsolver/android/CandyCrushSolverApplication.java new file mode 100644 index 0000000..8529ca1 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/CandyCrushSolverApplication.java @@ -0,0 +1,71 @@ +package com.applidium.candycrushsolver.android; + +import android.app.Application; +import android.content.SharedPreferences; + +import com.applidium.candycrushsolver.BuildConfig; +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.monitoring.di.ComponentManager; +import com.applidium.candycrushsolver.monitoring.di.crashes.CrashesComponent; +import com.applidium.candycrushsolver.monitoring.utils.aspect.RethrowUnexpectedAspect; +import com.applidium.candycrushsolver.monitoring.utils.aspect.ThreadingAspect; +import com.applidium.candycrushsolver.monitoring.utils.aspect.TracerAspect; + +import net.hockeyapp.android.CrashManager; + +import org.aspectj.lang.Aspects; + +import java.io.File; + +import timber.log.Timber; + +public class CandyCrushSolverApplication extends Application { + + protected static final String PREFS_NAME = "CANDYCRUSHSOLVER_PROJECT"; + protected static final boolean HOCKEY_APP_ENABLED = Settings.crashes.enabled; + protected static final String HOCKEY_APP_KEY = BuildConfig.APP_KEY; + + @Override + public void onCreate() { + super.onCreate(); + setupLogging(); + if (HOCKEY_APP_KEY.equals("")) { + return; + } + setupGraph(); + setupHockeyApp(); + setupAspects(); + } + + private void setupHockeyApp() { + if (!HOCKEY_APP_ENABLED) { + return; + } + CrashesComponent component = ComponentManager.getCrashesComponent(); + CrashManager.register(this, HOCKEY_APP_KEY, component.crashesListener()); + registerActivityLifecycleCallbacks(component.activityListener()); + registerComponentCallbacks(component.componentListener()); + } + + private void setupAspects() { + ThreadingAspect threadingAspect = Aspects.aspectOf(ThreadingAspect.class); + threadingAspect.init(); + TracerAspect loggerAspect = Aspects.aspectOf(TracerAspect.class); + loggerAspect.init(); + RethrowUnexpectedAspect rethrowAspect = Aspects.aspectOf(RethrowUnexpectedAspect.class); + rethrowAspect.init(); + } + + + protected void setupGraph() { + SharedPreferences preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + File cacheDirectory = getCacheDir(); + ComponentManager.init(preferences, cacheDirectory); + } + + private void setupLogging() { + if (BuildConfig.DEBUG) { + Timber.plant(new Timber.DebugTree()); + } + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/HeadLayer.java b/app/src/main/java/com/applidium/candycrushsolver/android/HeadLayer.java new file mode 100755 index 0000000..4f90480 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/HeadLayer.java @@ -0,0 +1,247 @@ +package com.applidium.candycrushsolver.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.support.v4.content.ContextCompat; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.Toast; + +import com.applidium.candycrushsolver.R; +import com.applidium.candycrushsolver.engine.Move; +import com.applidium.candycrushsolver.engine.Sweet; + +import java.util.List; + +import timber.log.Timber; + +/** + * Creates the head layer view which is displayed directly on window manager. + * It means that the view is above every application's view on your phone - + * until another application does the same. + */ +public class HeadLayer extends View { + + private static final int CANDY_SIZE = 45; + private static final int REDUCTION_SIZE = 10; + private final Context context; + private final FrameLayout frameLayout; + private WindowManager windowManager; + private ImageView image; + + private final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSLUCENT); + + public HeadLayer(Context context) { + super(context); + this.context = context; + frameLayout = new FrameLayout(this.context); + addToWindowManager(); + } + + private void addToWindowManager() { + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + windowManager.addView(frameLayout, params); + + LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + layoutInflater.inflate(R.layout.head, frameLayout); + image = (ImageView) frameLayout.findViewById(R.id.solution); + } + + public void destroy() { + if (frameLayout.isAttachedToWindow()) { + windowManager.removeView(frameLayout); + } + } + + public void showBestMoveOnScreen(Move best, float factorDiminutionSize) { + Bitmap bm = createBitmap(); + Timber.v("App cycle : just before display"); + //we divide sizes by REDUCTION_SIZE to make the bitmap creation faster + int sweetSize = Math.round(CANDY_SIZE * factorDiminutionSize) / REDUCTION_SIZE; + + boolean success = drawMove(best, factorDiminutionSize, bm, sweetSize); + if (!success) { + return; + } + showFinalImage(bm); + } + + public void showMovesOnScreen(List moves, float factorDiminutionSize) { + Timber.v("App cycle : just before display"); + Bitmap bm = createBitmap(); + //we divide sizes by REDUCTION_SIZE to make the bitmap creation faster + int sweetSize = Math.round(CANDY_SIZE * factorDiminutionSize) / REDUCTION_SIZE; + + for (Move move : moves) { + boolean success = drawMove(move, factorDiminutionSize, bm, sweetSize); + if (!success) { + return; + } + } + drawBorders(moves, bm, factorDiminutionSize, sweetSize); + showFinalImage(bm); + } + + private boolean drawMove(Move move, float factorDiminutionSize, Bitmap bm, int sweetSize) { + int x = (int) move.getSweet1().getX(); + int y = (int) move.getSweet1().getY(); + Timber.v("x is %d and y is %d", x, y); + if (move.getSweet1() == null || move.getSweet2() == null) { + Toast.makeText(context, getResources().getString(R.string.err_move), Toast.LENGTH_SHORT).show(); + return false; + } + Sweet.Type color = move.getSweet1().getType(); + Sweet.Type color2 = move.getSweet2().getType(); + + //we divide sizes by REDUCTION_SIZE to make the bitmap creation faster + int xReal = Math.round(x * factorDiminutionSize) / REDUCTION_SIZE; + int yReal = Math.round(y * factorDiminutionSize) / REDUCTION_SIZE; + + Timber.v("factor is %f", factorDiminutionSize); + Timber.v("xReal is %d and yReal is %d", xReal, yReal); + drawMoveOnBitmap(move, color, color2, xReal, yReal, bm, sweetSize); + return true; + } + + private void showFinalImage(Bitmap bm) { + windowManager.updateViewLayout(frameLayout, params); + if (image == null) { + return; + } + image.setVisibility(VISIBLE); + image.setImageBitmap(bm); + image.setImageAlpha(140); + image.setEnabled(false); + Timber.v("App cycle : just after display"); + } + + private Bitmap createBitmap() { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + params.width = size.x; + params.height = size.y; + + int statusBarHeight = getStatusBarHeight(); + int actionBarHeight = getActionBarHeight(); + + //we divide REDUCTION_SIZE by ten to make the bitmap creation faster + int bitmapWidth = size.x / REDUCTION_SIZE; + int bitmapHeight = (size.y - statusBarHeight - actionBarHeight) / REDUCTION_SIZE; + + Bitmap bm = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + bm = colorBitmapInBlack(bm); + return bm; + } + + private Bitmap colorBitmapInBlack(Bitmap bm) { + for (int i = 0; i < bm.getWidth(); i++) { + for (int j = 0; j < bm.getHeight(); j++) { + bm.setPixel(i, j, Color.rgb(0, 0, 0)); + } + } + return bm; + } + + private int getActionBarHeight() { + int actionBarHeight = 0; + int resource2 = getResources().getIdentifier("action_bar_height", "dimen", "android"); + if (resource2 > 0) { + actionBarHeight = context.getResources().getDimensionPixelSize(resource2); + } + return actionBarHeight; + } + + private int getStatusBarHeight() { + int statusBarHeight = 0; + int resource = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resource > 0) { + statusBarHeight = context.getResources().getDimensionPixelSize(resource); + } + return statusBarHeight; + } + + private void drawMoveOnBitmap(Move best, Sweet.Type color, Sweet.Type color2, int xReal, int yReal, Bitmap bm, int sweetSize) { + switch (best.findDirection()) { + case UP: + setWhitePixelsOnSolution(xReal, xReal + sweetSize, yReal - sweetSize, yReal + sweetSize, bm); + Timber.v("UP : %1$d %2$d, %3$s %4$s", xReal, yReal, color, color2); + break; + case DOWN: + setWhitePixelsOnSolution(xReal, xReal + sweetSize, yReal, yReal + sweetSize * 2, bm); + Timber.v("DOWN : %1$d %2$d, %3$s %4$s", xReal, yReal, color, color2); + break; + case LEFT: + setWhitePixelsOnSolution(xReal - sweetSize, xReal + sweetSize, yReal, yReal + sweetSize, bm); + Timber.v("LEFT : %1$d %2$d, %3$s %4$s", xReal, yReal, color, color2); + break; + case RIGHT: + setWhitePixelsOnSolution(xReal, xReal + sweetSize * 2, yReal, yReal + sweetSize, bm); + Timber.v("RIGHT : %1$d %2$d, %3$s %4$s", xReal, yReal, color, color2); + break; + default: + Toast.makeText(context, getResources().getString(R.string.err_move), Toast.LENGTH_LONG).show(); + } + } + + private void setWhitePixelsOnSolution(int xStart, int xStop, int yStart, int yStop, Bitmap bm) { + for (int i = xStart; i < xStop; i++) { + for (int j = yStart; j < yStop; j++) { + if (i > 0 && j > 0 && i < bm.getWidth() && j < bm.getHeight()) { + bm.setPixel(i, j, Color.rgb(255, 255, 255)); + } + } + } + } + + private void drawBorders(List moves, Bitmap bm, float factorDiminutionSize, int sweetSize) { + for (Move move : moves) { + int x = (int) move.getSweet1().getX(); + int y = (int) move.getSweet1().getY(); + + //we divide sizes by REDUCTION_SIZE to make the bitmap creation faster + int xReal = Math.round(x * factorDiminutionSize) / REDUCTION_SIZE; + int yReal = Math.round(y * factorDiminutionSize) / REDUCTION_SIZE; + + switch (move.findDirection()) { + case UP: + setBorderPixel(xReal, xReal + sweetSize, yReal - sweetSize, yReal + sweetSize, bm); + break; + case DOWN: + setBorderPixel(xReal, xReal + sweetSize, yReal, yReal + sweetSize * 2, bm); + break; + case LEFT: + setBorderPixel(xReal - sweetSize, xReal + sweetSize, yReal, yReal + sweetSize, bm); + break; + case RIGHT: + setBorderPixel(xReal, xReal + sweetSize * 2, yReal, yReal + sweetSize, bm); + break; + } + } + } + + private void setBorderPixel(int xStart, int xStop, int yStart, int yStop, Bitmap bm) { + for (int i = xStart; i < xStop; i++) { + bm.setPixel(i, yStart, ContextCompat.getColor(getContext(), R.color.colorBackground)); + bm.setPixel(i, yStop - 1, ContextCompat.getColor(getContext(), R.color.colorBackground)); + } + for (int j = yStart; j < yStop; j++) { + bm.setPixel(xStart, j, ContextCompat.getColor(getContext(), R.color.colorBackground)); + bm.setPixel(xStop - 1, j, ContextCompat.getColor(getContext(), R.color.colorBackground)); + } + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/HeadService.java b/app/src/main/java/com/applidium/candycrushsolver/android/HeadService.java new file mode 100755 index 0000000..3369465 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/HeadService.java @@ -0,0 +1,236 @@ +package com.applidium.candycrushsolver.android; + + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.NotificationCompat; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Toast; + +import com.applidium.candycrushsolver.R; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Date; + +import timber.log.Timber; + +public class HeadService extends AccessibilityService { + + private final static int FOREGROUND_ID = 999; + private static int DELAY = 5000; + private static final long TIME_LIMIT = 60000; + private static final String KEY_STRING_CHOICE = "KEY_STRING_CHOICE"; + private static final String PENDING_INTENT = "PENDING_INTENT"; + private String STORE_DIRECTORY; + final Handler handler = new Handler(); + private Context context; + private Intent intent; + private boolean choseBestMove; + private boolean beenInCandyCrush; + private boolean isStopped; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { //your attention space rangers, this is starCommand ! + choseBestMove = intent.getExtras().getBoolean(KEY_STRING_CHOICE); + toastServiceStarted(); + STORE_DIRECTORY = getFilesDir().getAbsolutePath(); + PendingIntent pendingIntent = createPendingIntent(); + Notification notification = createNotification(pendingIntent); + if (!choseBestMove) { + DELAY = 10000; + } + initializeBooleans(); + + startForeground(FOREGROUND_ID, notification); + + return START_NOT_STICKY; + } + + private void initializeBooleans() { + isStopped = false; + beenInCandyCrush = false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + destroyHeadService(); + deleteFiles(); + stopForeground(true); + toastServiceEnded(); + } + + private void initService() { + Timber.v("Life cycle : service initialisation"); + beenInCandyCrush = true; + context = this; + } + + private void destroyHeadService() { + if (intent != null) { + context.stopService(intent); + } + } + + private void deleteFiles() { + try { + if (STORE_DIRECTORY == null) { + return; + } + FileUtils.deleteDirectory(new File(STORE_DIRECTORY)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private PendingIntent createPendingIntent() { + Intent intent = new Intent(this, MainActivity.class); + return PendingIntent.getActivity(this, 0, intent, 0); + } + + private Notification createNotification(PendingIntent intent) { + NotificationCompat.Builder mBuilder = buildNotification(intent); + + PendingIntent quitPendingIntent = createPendingIntentToQuit(); + + NotificationCompat.Action actionOpenApp = + new NotificationCompat.Action.Builder(R.drawable.notif_yes, getText(R.string.notificationGo), intent).build(); + NotificationCompat.Action actionQuit = + new NotificationCompat.Action.Builder(R.drawable.notif_no, getText(R.string.notificationQuit), quitPendingIntent).build(); + + mBuilder.addAction(actionOpenApp); + mBuilder.addAction(actionQuit); + + return mBuilder.build(); + } + + private NotificationCompat.Builder buildNotification(PendingIntent intent) { + return new NotificationCompat.Builder(this) + .setContentTitle(getText(R.string.notificationTitle)) + .setContentText(getText(R.string.notificationText)) + .setSmallIcon(R.drawable.ic_launcher) + .setContentIntent(intent); + } + + private PendingIntent createPendingIntentToQuit() { + Intent quitIntent = new Intent(PENDING_INTENT); + PendingIntent quitPendingIntent = PendingIntent.getBroadcast(this, 0, quitIntent, PendingIntent.FLAG_CANCEL_CURRENT); + IntentFilter filter = new IntentFilter(); + filter.addAction(PENDING_INTENT); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + stopBusinessService(); + deleteFiles(); + isStopped = true; + stopForeground(true); + } + }; + registerReceiver(receiver, filter); + return quitPendingIntent; + } + + private void toastServiceStarted() { + String message; + if (choseBestMove) { + message = getResources().getString(R.string.start_service_best); + } else { + message = getResources().getString(R.string.start_service_every); + } + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + private void toastServiceEnded() { + Toast.makeText(this, getResources().getString(R.string.stop_service), Toast.LENGTH_SHORT).show(); + } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + + AccessibilityServiceInfo config = new AccessibilityServiceInfo(); + config.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; + config.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + + setServiceInfo(config); + } + + private void startImageListener() { + handler.postDelayed(getRunnable(), 2); + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (event.getSource() == null || isStopped) { + return; + } + Timber.v("Still on"); + File file = new File(STORE_DIRECTORY + "/myscreen.png"); + Date lastModDate = new Date(file.lastModified()); + if (file.exists()) { + launchBusinessServiceIfOnCandyCrush(event, lastModDate); + } + } + + private void launchBusinessServiceIfOnCandyCrush(AccessibilityEvent event, Date lastModDate) { + Date d = new Date(); + //check if screenshot not so old : way to know if screenshots working + if (event.getPackageName().toString().equals("com.king.candycrushsaga") && Math.abs(lastModDate.getTime() - d.getTime()) < TIME_LIMIT) { + launchBusinessService(); + } else { + stopBusinessService(); + } + } + + private void launchBusinessService() { + initService(); + Timber.v("App cycle : Candy Crush activity"); + startImageListener(); + } + + private void stopBusinessService() { + Timber.v("App cycle : activity other than Candy Crush"); + if (beenInCandyCrush) { + handler.removeCallbacksAndMessages(null); + Intent businessIntent = new Intent(this, BusinessService.class); + stopService(businessIntent); + } + } + + @Override + public void onInterrupt() { + super.onDestroy(); + destroyHeadService(); + deleteFiles(); + stopForeground(true); + toastServiceEnded(); + } + + private Runnable getRunnable() { + return new Runnable() { + public void run() { + Timber.v("App cycle : just before starting intent"); + intent = new Intent(context, BusinessService.class); + + Bundle extras = new Bundle(); + extras.putBoolean(KEY_STRING_CHOICE, choseBestMove); + intent.putExtras(extras); + + context.startService(intent); + Timber.v("App cycle : just after starting intent"); + handler.postDelayed(getRunnable(), DELAY); + } + }; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/MainActivity.java b/app/src/main/java/com/applidium/candycrushsolver/android/MainActivity.java new file mode 100755 index 0000000..c249bdd --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/MainActivity.java @@ -0,0 +1,71 @@ +package com.applidium.candycrushsolver.android; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; + +import com.applidium.candycrushsolver.BuildConfig; +import com.applidium.candycrushsolver.R; + +import net.hockeyapp.android.metrics.MetricsManager; + +import org.opencv.android.OpenCVLoader; + +import timber.log.Timber; + +public class MainActivity extends AppCompatActivity { + + private void initializeOpenCV() { + if (!OpenCVLoader.initDebug()) { + Timber.i("open cv initialization failed"); + initializeOpenCV(); + } else { + Timber.i("open cv initialization successful"); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + showTutorialIfFirstTime(); + initializeOpenCV(); + + if (!(BuildConfig.APP_KEY).equals("")) { + MetricsManager.register(this, getApplication(), BuildConfig.APP_KEY); + } + + launchSettingsFragment(); + } + + private void launchSettingsFragment() { + SettingsFragment settingsFragment = new SettingsFragment(); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, settingsFragment) + .commit(); + } + + private void showTutorialIfFirstTime() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + boolean previouslyStarted = prefs.getBoolean(getString(R.string.pref_previously_started), false); + if(!previouslyStarted) { + SharedPreferences.Editor edit = prefs.edit(); + edit.putBoolean(getString(R.string.pref_previously_started), Boolean.TRUE); + edit.apply(); + Intent intent = new Intent(this, TutoActivity.class); + startActivity(intent); + } + } + + @Override + public void onBackPressed() { + Timber.v("exit app"); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_HOME); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/PermissionChecker.java b/app/src/main/java/com/applidium/candycrushsolver/android/PermissionChecker.java new file mode 100755 index 0000000..379226f --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/PermissionChecker.java @@ -0,0 +1,38 @@ +package com.applidium.candycrushsolver.android; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; + +public class PermissionChecker { + + private final Context context; + + public PermissionChecker(Context context) { + this.context = context; + } + + @TargetApi(Build.VERSION_CODES.M) + public boolean isRequiredPermissionGranted() { + if(isMarshmallowOrHigher()) { + return Settings.canDrawOverlays(context); + } + return true; + } + + @TargetApi(Build.VERSION_CODES.M) + public Intent createRequiredPermissionIntent() { + if(isMarshmallowOrHigher()) { + return new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + context.getPackageName())); + } + return null; + } + + private boolean isMarshmallowOrHigher() { + return android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/Screenshot.java b/app/src/main/java/com/applidium/candycrushsolver/android/Screenshot.java new file mode 100644 index 0000000..6e2d31d --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/Screenshot.java @@ -0,0 +1,268 @@ +package com.applidium.candycrushsolver.android; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.Image; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.annotation.NonNull; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.OrientationEventListener; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import timber.log.Timber; + +public class Screenshot { + + private static final String SCREENCAP_NAME = "screencap"; + private static final String THREAD_SCREENSHOT = "Thread screenshot"; + private static final int VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; + private static final int DELAY = 1000; + private static String STORE_DIRECTORY; + private static MediaProjection mediaProjection; + private MediaProjectionManager projectionManager; + private ImageReader imageReader; + private Handler handler; + private Display display; + private VirtualDisplay virtualDisplay; + private DisplayMetrics metrics; + private int density; + private int width; + private int height; + private int rotation; + private OrientationChangeCallback orientationChangeCallback; + + /******************************************* Lifecycle *******************************/ + + public Screenshot(String storeDirectory) { + STORE_DIRECTORY = storeDirectory; + } + + public void callForTheProjectionManager(Activity activity) { + projectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE); + Timber.v("Manager"); + } + + public void afterActivityResult(Activity activity, int resultCode, Intent data) { + Timber.v("Activity result"); + mediaProjection = projectionManager.getMediaProjection(resultCode, data); + + if (mediaProjection != null) { + if(!isDirectoryCreationSuccessful()) { + Timber.e("failed to create file storage directory."); + return; + } + + displayMetrics(activity); + + // create virtual display depending on device width / height + createVirtualDisplay(); + + registerOrientationChangeCallback(activity); + + mediaProjection.registerCallback(new MediaProjectionStopCallback(), handler); + } + } + + private boolean isDirectoryCreationSuccessful(){ + File storeDirectory = new File(STORE_DIRECTORY); + if (!storeDirectory.exists()) { + return storeDirectory.mkdirs(); + } + return true; + } + + private void registerOrientationChangeCallback(Activity activity) { + orientationChangeCallback = new OrientationChangeCallback(activity); + if (orientationChangeCallback.canDetectOrientation()) { + orientationChangeCallback.enable(); + } + } + + private void displayMetrics(Activity activity) { + metrics = activity.getResources().getDisplayMetrics(); + density = metrics.densityDpi; + display = activity.getWindowManager().getDefaultDisplay(); + Timber.v("Metrics displayed"); + } + + public void startCaptureHandlingThread() { + HandlerThread thread = new HandlerThread(THREAD_SCREENSHOT); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + public Intent createCaptureIntent() { + return projectionManager.createScreenCaptureIntent(); + } + + + public void stopHandler() { + handler.post(new Runnable() { + @Override + public void run() { + if (mediaProjection != null) { + mediaProjection.stop(); + } + } + }); + } + + /******************************************* Factoring Virtual Display creation ****************/ + + private void createVirtualDisplay() { + // get width and height + Point size = new Point(); + display.getSize(size); + width = size.x; + height = size.y; + + // start capture reader + imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2); + virtualDisplay = mediaProjection.createVirtualDisplay(SCREENCAP_NAME, width, height, density, VIRTUAL_DISPLAY_FLAGS, imageReader.getSurface(), null, handler); + + handler.postDelayed(getRunnableThatTakesScreenshot(), 2); + } + + private Runnable getRunnableThatTakesScreenshot() { + return new Runnable() { + @Override + public void run() { + Image image = null; + FileOutputStream fos = null; + Bitmap bitmap = null; + + try { + image = imageReader.acquireLatestImage(); + if (image != null) { + Image.Plane[] planes = image.getPlanes(); + ByteBuffer buffer = planes[0].getBuffer(); + int pixelStride = planes[0].getPixelStride(); + int rowStride = planes[0].getRowStride(); + int rowPadding = rowStride - pixelStride * width; + + bitmap = createBitmap(buffer, pixelStride, rowPadding); + Bitmap resizedBitmap = resizeBitmap(bitmap); + fos = writeBitmapToFile(resizedBitmap); + } + + } catch (Exception e) { + e.printStackTrace(); + mediaProjection.stop(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + if (bitmap != null) { + bitmap.recycle(); + } + if (image != null) { + image.close(); + } + handler.postDelayed(getRunnableThatTakesScreenshot(), DELAY); + } + } + }; + } + + @NonNull + private Bitmap resizeBitmap(Bitmap bitmap) { + float d = metrics.density; + int newHeight = Math.round(bitmap.getHeight() / d); + int newWidth = Math.round(bitmap.getWidth() / d); + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, false); + } + + @NonNull + private FileOutputStream writeBitmapToFile(Bitmap bitmap) throws FileNotFoundException { + FileOutputStream fos = new FileOutputStream(STORE_DIRECTORY + "/myscreen.png"); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); + return fos; + } + + @NonNull + private Bitmap createBitmap(ByteBuffer buffer, int pixelStride, int rowPadding) { + Bitmap bitmap; + bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(buffer); + return bitmap; + } + + private class OrientationChangeCallback extends OrientationEventListener { + public OrientationChangeCallback(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + synchronized (this) { + final int rotation = display.getRotation(); + if (rotation != Screenshot.this.rotation) { + Screenshot.this.rotation = rotation; + try { + cleanUpVirtualDisplay(); + cleanUpImageReader(); + + // re-create virtual display depending on device width / height + createVirtualDisplay(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private void cleanUpVirtualDisplay() { + if (virtualDisplay != null) { + virtualDisplay.release(); + } + } + + private void cleanUpImageReader() { + if (imageReader != null) { + imageReader.setOnImageAvailableListener(null, null); + } + } + + private void disableOrientationChangeCallback() { + if (orientationChangeCallback != null) { + orientationChangeCallback.disable(); + } + } + + private class MediaProjectionStopCallback extends MediaProjection.Callback { + @Override + public void onStop() { + Timber.e("stopping projection"); + handler.post(new Runnable() { + @Override + public void run() { + cleanUpVirtualDisplay(); + cleanUpImageReader(); + disableOrientationChangeCallback(); + mediaProjection.unregisterCallback(MediaProjectionStopCallback.this); + } + }); + } + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/SettingsFragment.java b/app/src/main/java/com/applidium/candycrushsolver/android/SettingsFragment.java new file mode 100755 index 0000000..bac81ab --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/SettingsFragment.java @@ -0,0 +1,361 @@ +package com.applidium.candycrushsolver.android; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.accessibility.AccessibilityManager; +import android.widget.Toast; + +import com.applidium.candycrushsolver.R; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +import timber.log.Timber; + +public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + private static final long TIME_LIMIT = 2000; + private static final int REQUEST_CODE_FOR_SCREENSHOT = 100; + private static final String KEY_STRING_CHOICE = "KEY_STRING_CHOICE"; + private static String STORE_DIRECTORY; + private PermissionChecker permissionChecker; + private Preference headService; + private Preference buttonScreenshot; + private Preference buttonAccessibility; + private Preference goButton; + private ListPreference choiceButton; + private boolean currentlyRecording = false; + private boolean serviceAlreadyStarted = false; + private static Screenshot screenshot; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + STORE_DIRECTORY = getActivity().getFilesDir().getAbsolutePath(); + Timber.v("Store directory : %s", STORE_DIRECTORY); + + addPreferencesFromResource(R.xml.settings); + + initializeHeadServiceButton(); + initializeAccessibilityButton(); + initializeCandyCrushButton(); + initializeTutorialButton(); + initializeChoiceButton(); + + /*** Screenshot part ***/ + screenshot = new Screenshot(STORE_DIRECTORY); + screenshot.callForTheProjectionManager(getActivity()); + initializeScreenshotButton(); + screenshot.startCaptureHandlingThread(); + } + + private void initializeHeadServiceButton() { + headService = findPreference(getString(R.string.serviceEnabledKey)); + permissionChecker = new PermissionChecker(getActivity()); + headService.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //is Marshmallow or higher + Intent intent = permissionChecker.createRequiredPermissionIntent(); + startActivity(intent); + } else { + Toast.makeText(getActivity(), getResources().getString(R.string.api_under_23), Toast.LENGTH_LONG).show(); + } + return true; + } + }); + } + + private void initializeAccessibilityButton() { + buttonAccessibility = findPreference(getString(R.string.accessibility_title)); + buttonAccessibility.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + startActivity(intent); + return true; + } + }); + } + + private void initializeScreenshotButton() { + buttonScreenshot = findPreference(getString(R.string.screenshot)); + buttonScreenshot.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + if (!currentlyRecording) { + startProjection(); + currentlyRecording = true; + } else { + stopProjection(); + currentlyRecording = false; + changeIconAccordingToCurrentSettings(); + } + return true; + } + }); + } + + private void initializeCandyCrushButton() { + goButton = findPreference(getString(R.string.go)); + goButton.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + String candyCrushPackageName = "com.king.candycrushsaga"; + Intent intent = new Intent(); + intent.setComponent(new ComponentName(candyCrushPackageName, "com.king.candycrushsaga.CandyCrushSagaActivity")); + + if (isCandyCrushInstalled(intent)) { + startActivity(intent); + } else { + Toast.makeText(getActivity(), getResources().getString(R.string.candy_not_installed), Toast.LENGTH_LONG).show(); + try { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + candyCrushPackageName))); + } catch (android.content.ActivityNotFoundException noPlayStoreOnPhone) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + candyCrushPackageName))); + } + } + return true; + } + }); + } + + private boolean isCandyCrushInstalled(Intent intent) { + PackageManager manager = getActivity().getPackageManager(); + List info = manager.queryIntentActivities(intent, 0); + return info.size() > 0; + } + + private void initializeTutorialButton() { + Preference tutorialButton = findPreference(getString(R.string.tuto)); + tutorialButton.setIcon(R.drawable.question); + tutorialButton.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(getActivity(), TutoActivity.class); + startActivity(intent); + return true; + } + }); + } + + private void initializeChoiceButton() { + choiceButton = (ListPreference) findPreference(getString(R.string.choice_title)); + changeButtonAppearanceAccordingToChoice(getResources().getString(R.string.choice_best)); + } + + private void changeChoiceButton() { + SharedPreferences settings = getPreferenceManager().getSharedPreferences(); + String choice = choiceButton.getEntry().toString(); + Timber.v(choice); + + SharedPreferences.Editor editor = settings.edit(); + editor.putString(getString(R.string.choice_best), choice); + editor.apply(); + + changeButtonAppearanceAccordingToChoice(choice); + } + + private void changeButtonAppearanceAccordingToChoice(String choice) { + if (choice.equals(getResources().getString(R.string.choice_every))) { + choiceButton.setIcon(R.drawable.infinite); + } else { + choiceButton.setIcon(R.drawable.star); + } + } + + public void startProjection() { + Timber.v("Start projection"); + startActivityForResult(screenshot.createCaptureIntent(), REQUEST_CODE_FOR_SCREENSHOT); + } + + public void stopProjection() { + Timber.v("Stop projection"); + screenshot.stopHandler(); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + Timber.v("On activity result"); + switch (requestCode) { + case REQUEST_CODE_FOR_SCREENSHOT: + Timber.v("Right request code"); + screenshot.afterActivityResult(getActivity(), resultCode, data); + } + } + + @Override + public void onResume() { + super.onResume(); + serviceAlreadyStarted = false; + if (isRecentFilePresent()) { + currentlyRecording = true; + } + changeIconAccordingToCurrentSettings(); + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + changeChoiceButton(); + changeIconAccordingToCurrentSettings(); + } + + private void changeIconAccordingToCurrentSettings() { + setIconColorStepOne(); + setIconColorStepTwo(); + setIconColorStepThree(); + + if (threeStepsOK()) { + setServiceOnState(); + } else { + setServiceOffState(); + } + } + + private void setServiceOffState() { + goButton.setIcon(R.drawable.ic_launcher_wb); + goButton.setSelectable(false); + if (serviceAlreadyStarted) { + stopHeadService(); + serviceAlreadyStarted = false; + } + } + + private void setServiceOnState() { + goButton.setIcon(R.drawable.ic_launcher); + goButton.setSelectable(true); + if (!serviceAlreadyStarted) { + startHeadService(); + serviceAlreadyStarted = true; + } + } + + private boolean threeStepsOK() { + return permissionChecker.isRequiredPermissionGranted() && isAccessibilityEnabled() && currentlyRecording; + } + + private void setIconColorStepThree() { + if (currentlyRecording) { + buttonScreenshot.setIcon(R.drawable.three); + buttonScreenshot.setTitle(R.string.screenshot_stop); + } else { + buttonScreenshot.setIcon(R.drawable.three_red); + buttonScreenshot.setTitle(R.string.screenshot); + } + } + + private void setIconColorStepTwo() { + if (isAccessibilityEnabled()) { + buttonAccessibility.setIcon(R.drawable.two); + } else { + buttonAccessibility.setIcon(R.drawable.two_red); + } + } + + private void setIconColorStepOne() { + if (permissionChecker.isRequiredPermissionGranted()) { + headService.setIcon(R.drawable.one); + } else { + headService.setIcon(R.drawable.one_red); + } + } + + public boolean isAccessibilityEnabled() { + final String accessibilityServiceName = getActivity().getPackageName() + "/com.applidium.candycrushsolver.android.HeadService"; + int accessibilityEnabled = getGeneralAccessibilityState(); + TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); + if (accessibilityEnabled == 1) { + String settingValue = Settings.Secure.getString(getActivity().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + if (settingValue != null) { + mStringColonSplitter.setString(settingValue); + while (mStringColonSplitter.hasNext()) { + String accessibilityService = mStringColonSplitter.next(); + if (accessibilityService.equalsIgnoreCase(accessibilityServiceName)) { + return true; + } + } + } + } + return false; + } + + private int getGeneralAccessibilityState() { + int accessibilityEnabled = 0; + try { + accessibilityEnabled = Settings.Secure.getInt(getActivity().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED); + } catch (Settings.SettingNotFoundException e) { + Timber.d("Error finding setting, default accessibility to not found: %s", e.getMessage()); + } + return accessibilityEnabled; + } + + private boolean isRecentFilePresent() { + File file = new File(STORE_DIRECTORY + "/myscreen.png"); + Date lastModDate = new Date(file.lastModified()); + if (file.exists()) { + Date d = new Date(); + return Math.abs(lastModDate.getTime() - d.getTime()) < TIME_LIMIT; + } + return false; + } + + private void deleteOldScreenshots() { + try { + STORE_DIRECTORY = getActivity().getFilesDir().getAbsolutePath(); + if (STORE_DIRECTORY == null) { + return; + } + FileUtils.deleteDirectory(new File(STORE_DIRECTORY)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void startHeadService() { + String choice = choiceButton.getEntry().toString(); + Boolean choseBestMove = (choice.equals(getResources().getString(R.string.choice_best))); + + Context context = getActivity(); + Intent intent = new Intent(context, HeadService.class); + + Bundle extras = new Bundle(); + extras.putBoolean(KEY_STRING_CHOICE, choseBestMove); + intent.putExtras(extras); + + context.startService(intent); + } + + private void stopHeadService() { + deleteOldScreenshots(); + AccessibilityManager manager = (AccessibilityManager) getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE); + manager.interrupt(); + } + + +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/TutoActivity.java b/app/src/main/java/com/applidium/candycrushsolver/android/TutoActivity.java new file mode 100644 index 0000000..a531198 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/TutoActivity.java @@ -0,0 +1,67 @@ +package com.applidium.candycrushsolver.android; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; + +import com.applidium.candycrushsolver.R; +import com.viewpagerindicator.CirclePageIndicator; + +public class TutoActivity extends FragmentActivity { + + private static final int NUM_PAGES = 3; + private ViewPager pager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_tuto); + + pager = (ViewPager) findViewById(R.id.pager); + PagerAdapter pagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + pager.setAdapter(pagerAdapter); + + CirclePageIndicator indicator = (CirclePageIndicator) findViewById(R.id.indicator); + indicator.setViewPager(pager); + } + + @Override + public void onBackPressed() { + if (pager.getCurrentItem() == 0) { + // If the user is currently looking at the first step, allow the system to handle the + // Back button. This calls finish() on this activity and pops the back stack. + super.onBackPressed(); + } else { + // Otherwise, select the previous step. + pager.setCurrentItem(pager.getCurrentItem() - 1); + } + } + + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + public ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch(position) { + case 0: + return new TutoFragment1(); + case 1: + return new TutoFragment2(); + case 2: + return new TutoFragment3(); + } + return null; + } + + @Override + public int getCount() { + return NUM_PAGES; + } + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment1.java b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment1.java new file mode 100644 index 0000000..d0beb4d --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment1.java @@ -0,0 +1,17 @@ +package com.applidium.candycrushsolver.android; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.applidium.candycrushsolver.R; + +public class TutoFragment1 extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_tuto1, container, false); + } +} + diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment2.java b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment2.java new file mode 100644 index 0000000..0b3330e --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment2.java @@ -0,0 +1,16 @@ +package com.applidium.candycrushsolver.android; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.applidium.candycrushsolver.R; + +public class TutoFragment2 extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_tuto2, container, false); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment3.java b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment3.java new file mode 100644 index 0000000..bc52450 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/android/TutoFragment3.java @@ -0,0 +1,33 @@ +package com.applidium.candycrushsolver.android; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.applidium.candycrushsolver.R; + +public class TutoFragment3 extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_tuto3, container, false); + + Button button = (Button) rootView.findViewById(R.id.tuto_button); + if (button == null) { + return rootView; + } + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(getActivity(), MainActivity.class); + startActivity(intent); + getActivity().finish(); + } + }); + + return rootView; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesExtractor.java b/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesExtractor.java new file mode 100644 index 0000000..bdb665e --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesExtractor.java @@ -0,0 +1,239 @@ +package com.applidium.candycrushsolver.engine; + +import android.content.res.Configuration; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import timber.log.Timber; + +public class FeaturesExtractor { + private static final int NB_SWEETS_IN_COL = 16; + private static final int WALK = 6; + private static double THRESHOLD; + private static int STEP = 30; + private static final int OTHER_PIXEL = 10; + private static int DIFFERENCE_LIMIT = 20; + private static final int DIFFERENCE_LIMIT_OTHER = 45; + + public List> extractFeaturesFromImage(Mat img, int [] features, int orientation) { + Timber.v("Running Template Matching"); + List messySweets = new ArrayList<>(); + for (int i = 0; i < features.length; i++) { + Timber.v("Template matching"); + messySweets.addAll(extractSweetsForFeature(img, features[i], i, orientation)); + Timber.v("Template matching finished"); + } + + deleteCloseOnes(messySweets); + + double colStep = img.height() / NB_SWEETS_IN_COL; + List> grid = sortSweets(messySweets, colStep); + alignGrid(grid, colStep); + readjustGridWithSymmetry(grid); + + printFinalGrid(grid); + + return grid; + } + + /******************************************* OpenCV Version *******************************/ + + public List extractSweetsForFeatureWithOpenCV(Mat img, Mat feature, int i) { + STEP = 10; + DIFFERENCE_LIMIT = 60; + Imgproc.pyrDown(feature, feature, new Size(feature.cols() / 2, feature.rows() / 2)); + Mat result = createResultMat(img, feature); + result = matchFeature(img, feature, result); + if (i != 5) { + THRESHOLD = 0.96; + } else { + THRESHOLD = 0.85; + } + + Core.MinMaxLocResult mmr = Core.minMaxLoc(result); + Point matchLoc = mmr.maxLoc; + int refX = (int) matchLoc.x + feature.cols() / 2; + int refY = (int) matchLoc.y + feature.rows() / 2; + if (!isFeatureAbsent(img, feature, refX, refY)) { + Imgproc.threshold(result, result, THRESHOLD, 255, Imgproc.THRESH_BINARY); + return extractSweetsForFeatureWithOpenCV(result, i); + } + return Collections.emptyList(); + } + + private List extractSweetsForFeatureWithOpenCV(Mat result, int colorIndex) { + List featureSweets = new ArrayList<>(); + result.convertTo(result, CvType.CV_64FC1); + for (int row = 0; row < result.rows(); row++) { + for (int col = 0; col < result.cols(); col++) { + double[] bgr = new double[3]; + result.get(row, col, bgr); + if (bgr[0] > THRESHOLD) { + Sweet sweet = new Sweet(colorIndex, new Point(col, row)); + featureSweets.add(sweet); + } + } + } + return featureSweets; + } + + private static boolean isFeatureAbsent(Mat img, Mat feature, int refX, int refY) { + return Math.abs(img.get(refY, refX)[0] - feature.get(feature.cols() / 2, feature.rows() / 2)[0]) > DIFFERENCE_LIMIT + || Math.abs(img.get(refY, refX)[1] - feature.get(feature.cols() / 2, feature.rows() / 2)[1]) > DIFFERENCE_LIMIT + || Math.abs(img.get(refY, refX)[2] - feature.get(feature.cols() / 2, feature.rows() / 2)[2]) > DIFFERENCE_LIMIT; + } + + protected Mat createResultMat(Mat img, Mat feature) { + int result_cols = img.cols() - feature.cols() + 1; + int result_rows = img.rows() - feature.rows() + 1; + return new Mat(result_rows, result_cols, CvType.CV_32FC1); + } + + protected Mat matchFeature(Mat img, Mat feature, Mat result) { + Imgproc.matchTemplate(img, feature, result, Imgproc.TM_CCOEFF_NORMED); + Core.normalize(result, result, 0, 1, Core.NORM_MINMAX, -1, new Mat()); + return result; + } + + /******************************************* Without OpenCV Version *******************************/ + + private List extractSweetsForFeature(Mat img, int feature, int i, int orientation) { + List featureSweets = new ArrayList<>(); + int rowLimit = adjustLimitAccordingToOrientation(orientation, img.rows()); + for (int k = 0; k < rowLimit; k += WALK) { + for (int l = 0; l < img.cols(); l += WALK) { + lookForFeatureInPixel(img, feature, i, featureSweets, k, l); + } + } + return featureSweets; + } + + private int adjustLimitAccordingToOrientation(int orientation, int rows) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return rows; + } + return (int) (rows * 0.8); + } + + private void lookForFeatureInPixel(Mat img, int feature, int i, List featureSweets, int k, int l) { + if (isTheSameColor(img, feature , k, l, DIFFERENCE_LIMIT)) { + if (k + OTHER_PIXEL < img.rows() && l + OTHER_PIXEL < img.cols()) { + //second check a little further to be more precise + if (isTheSameColor(img, feature, k + OTHER_PIXEL, l + OTHER_PIXEL, DIFFERENCE_LIMIT_OTHER)) { + Sweet sweet = new Sweet(i, new Point(l, k)); + featureSweets.add(sweet); + } + } + } + } + + private static boolean isTheSameColor(Mat img, int reference, int k, int l, int difference) { + int redReference = (reference >> 16) & 0xFF; + int greenReference = (reference >> 8) & 0xFF; + int blueReference = (reference >> 0) & 0xFF; + return Math.abs(img.get(k, l)[0] - blueReference) < difference + && Math.abs(img.get(k, l)[1] - greenReference) < difference + && Math.abs(img.get(k, l)[2] - redReference) < difference; + } + + /******************************************* Shared functions *******************************/ + + private List> sortSweets(List messySweets, double colStep) { + List> grid = new ArrayList<>(); + + // Get minHeight and minWidth + int minHeight = Integer.MAX_VALUE; + int minWidth = Integer.MAX_VALUE; + for (Sweet sweet : messySweets) { + if (sweet.getY() < minHeight) { + minHeight = (int) sweet.getY(); + } + if (sweet.getX() < minWidth) { + minWidth = (int) sweet.getX(); + } + } + + fillGrid(messySweets, colStep, grid, minHeight, minWidth); + return grid; + } + + private void fillGrid(List messySweets, double colStep, List> grid, int minHeight, int minWidth) { + for (Sweet sweet : messySweets) { + addSweetToGrid(grid, sweet, minHeight - 10, minWidth - 10, colStep); + } + } + + private void readjustGridWithSymmetry(List> grid) { + int highestLineSize = findHighestLineSize(grid); + rearrangeRows(grid, highestLineSize); + } + + private void rearrangeRows(List> grid, int highestLineSize) { + for (int i = 0; i < grid.size(); i++) { + while (highestLineSize > grid.get(i).size() && grid.get(i).size() != 0) { + grid.get(i).add(null); + } + } + } + + private int findHighestLineSize(List> grid) { + int highestLineSize = 0; + for (int i = 0; i < grid.size(); i++) { + if (highestLineSize < grid.get(i).size()) { + highestLineSize = grid.get(i).size(); + } + } + return highestLineSize; + } + + private void alignGrid(List> grid, double colStep) { + for (int i = 0; i < grid.size(); i++) { + for (int j = 0; j < grid.get(i).size() - 1; j++) { + if (grid.get(i).get(j) != null && grid.get(i).get(j + 1) != null && Math.abs(grid.get(i).get(j).getX() - grid.get(i).get(j + 1).getX()) > colStep * 1.5) { + grid.get(i).add(j + 1, null); + } + } + } + } + + private void printFinalGrid(List> grid) { + for (int i = 0; i < grid.size(); i++) { + Timber.v("%s : %s", i, grid.get(i).toString()); + } + } + + + private static void deleteCloseOnes(List featureSweets) { + for (int i = 0; i < featureSweets.size() - 1; i++) { + for (int j = i + 1; j < featureSweets.size(); j++) { + boolean xClose = Math.abs(featureSweets.get(i).getX() - featureSweets.get(j).getX()) < STEP; + boolean yClose = Math.abs(featureSweets.get(i).getY() - featureSweets.get(j).getY()) < STEP; + if (xClose && yClose) { + featureSweets.remove(featureSweets.get(j)); + j--; + } + } + } + } + + private void addSweetToGrid(List> grid, Sweet sweet, int firstPositionRow, int firstPositionCol, double colStep) { + int row = (int) (Math.floor(sweet.getY() - firstPositionRow) / colStep) + 1; + int col = (int) (Math.floor(sweet.getX() - firstPositionCol) / (colStep - 1)); + while (row >= grid.size()) { + grid.add(new ArrayList()); + } + while (col >= grid.get(row).size()) { + grid.get(row).add(null); + } + grid.get(row).set(col, sweet); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesPainter.java b/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesPainter.java new file mode 100644 index 0000000..9670904 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/engine/FeaturesPainter.java @@ -0,0 +1,40 @@ +package com.applidium.candycrushsolver.engine; + +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +import java.util.List; + +public class FeaturesPainter { + + private final static int FEATURE_SIZE = 35; + + public static void drawRectanglesFromMessySweets(List messySweets, Mat img) { + for (Sweet sweet : messySweets) { + drawRectangleForSweet(img, sweet); + } + } + + public static void drawRectanglesFromSweetsGrid(List> grid, Mat img) { + if (grid == null) { + return; + } + for (List row : grid) { + for (Sweet sweet : row) { + drawRectangleForSweet(img, sweet); + } + } + } + + private static void drawRectangleForSweet(Mat img, Sweet sweet) { + if (sweet == null) { + return; + } + double col = sweet.getX(); + double row = sweet.getY(); + Scalar colorant = sweet.getType().color; + Imgproc.rectangle(img, new Point(col, row), new Point(col + FEATURE_SIZE, row + FEATURE_SIZE), colorant); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/engine/Move.java b/app/src/main/java/com/applidium/candycrushsolver/engine/Move.java new file mode 100644 index 0000000..c71bec7 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/engine/Move.java @@ -0,0 +1,52 @@ +package com.applidium.candycrushsolver.engine; + +public class Move { + public enum Direction { UP, DOWN, LEFT, RIGHT } //the direction is seen as sweet1 point of view + + private final Sweet sweet1; + private final Sweet sweet2; + + /* CALCULATING SCORES (not real score in the game, personal score to fond the best move) : + 1 : plain move + 2 : move that can make other moves with falling sweets (or +1 point if the move is better than 1) + 3 : create a 4-special horizontal sweet (with horizontal stripes) + 4 : create a 4-special vertical sweet (with vertical stripes) + 5 : create a special bomb sweet + 7 : create a 5-special sweet + */ + private int score; + + public Move(Sweet sweet1, Sweet sweet2, int score) { + this.sweet1 = sweet1; + this.sweet2 = sweet2; + this.score = score; + } + + public Sweet getSweet1() { + return sweet1; + } + + public Sweet getSweet2() { + return sweet2; + } + + public int getScore() { + return score; + } + + public void addToScore(int nb) { + score += nb; + } + + public Direction findDirection() { + if (sweet1 == null || sweet2 == null) { + return null; + } + double horizontalDiff = sweet1.getX() - sweet2.getX(); + double verticalDiff = sweet1.getY() - sweet2.getY(); + if (Math.abs(verticalDiff) > Math.abs(horizontalDiff)) { + return verticalDiff > 0 ? Direction.UP : Direction.DOWN; + } + return horizontalDiff > 0 ? Direction.LEFT : Direction.RIGHT; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/engine/MoveFinder.java b/app/src/main/java/com/applidium/candycrushsolver/engine/MoveFinder.java new file mode 100644 index 0000000..f951b52 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/engine/MoveFinder.java @@ -0,0 +1,326 @@ +package com.applidium.candycrushsolver.engine; + +import java.util.ArrayList; +import java.util.List; + +public class MoveFinder { + + private List> grid = new ArrayList<>(); + private final List moves = new ArrayList<>(); + private boolean moveAlreadyAdded; + private boolean symmetricalMoveDone; + private Move.Direction symmetricalDirection; + private boolean couldBeAFiveSpecialMove; + //2 booleans for bomb special move + private boolean bombTwoVertical; + private boolean bombTwoHorizontal; + + public MoveFinder(List> grid) { + this.grid = grid; + } + + public Move findMove() { + findAllMoves(); + return findBestMove(); + } + + public void findAllMoves() { + for (int i = 0; i < grid.size(); i++) { + for (int j = 0; j < grid.get(i).size(); j++) { + manageMove(i, j); + } + } + } + + private void manageMove(int i, int j) { + reinitializeSymmetricalBooleans(); + //does the bottom cell exists ? + if (canGoDown(i, j)) { + checkDirectionDown(grid.get(i).get(j), i + 1, j); + checkDirectionUp(grid.get(i + 1).get(j), i, j); + } + reinitializeSymmetricalBooleans(); + //does the right cell exists ? + if (canGoRight(i, j)) { + checkDirectionRight(grid.get(i).get(j), i, j + 1); + checkDirectionLeft(grid.get(i).get(j + 1), i, j); + } + } + + private void reinitializeSymmetricalBooleans() { + symmetricalMoveDone = false; + symmetricalDirection = null; + } + + private void checkDirectionUp(Sweet reference, int i, int j) { + initializeBooleansToFindSpecialMoves(); + checkMatchUpUp(reference, i, j); + checkMatchVerticalLeft(reference, i, j); + checkMatchVerticalRight(reference, i, j); + } + + private void checkMatchVerticalRight(Sweet reference, int i, int j) { + boolean couldBeFourVerticalMove = false; + if (sameColorOnYourRight(reference, i, j)) { + if (sameColorOnYourRight(reference, i, j + 1)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + couldBeFourVerticalMove = true; + } else if (couldBeAFiveSpecialMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(3); + } else if (bombTwoVertical && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.DOWN) { + moves.get(moves.size() - 1).addToScore(1); + } + } + if (sameColorOnYourLeft(reference, i, j)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + } else if (couldBeFourVerticalMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(3); + } else if (bombTwoVertical && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.DOWN) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + } + + private void checkMatchVerticalLeft(Sweet reference, int i, int j) { + boolean couldBeFourVerticalMove = false; + if (sameColorOnYourLeft(reference, i, j)) { + if (sameColorOnYourLeft(reference, i, j - 1)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + couldBeFourVerticalMove = true; + couldBeAFiveSpecialMove = true; + } else if (bombTwoVertical && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.DOWN) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + if (sameColorOnYourRight(reference, i, j)) { + if (couldBeFourVerticalMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(3); + } + } + } + + private void checkMatchUpUp(Sweet reference, int i, int j) { + if (sameColorAbove(reference, i, j)) { + if (sameColorAbove(reference, i - 1, j) && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + bombTwoVertical = true; + } + if (sameColorAbove(reference, i - 1, j) && symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.DOWN) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + + private void checkDirectionDown(Sweet reference, int i, int j) { + initializeBooleansToFindSpecialMoves(); + checkMatchDownDown(reference, i, j); + checkMatchVerticalLeft(reference, i, j); + checkMatchVerticalRight(reference, i, j); + } + + private void checkMatchDownDown(Sweet reference, int i, int j) { + if (sameColorUnder(reference, i, j)) { + if (sameColorUnder(reference, i + 1, j)) { + addThisMove(reference, grid.get(i).get(j)); + bombTwoVertical = true; + } + } + } + + private void checkDirectionLeft(Sweet reference, int i, int j) { + initializeBooleansToFindSpecialMoves(); + checkMatchLeftLeft(reference, i, j); + checkMatchHorizontalUp(reference, i, j); + checkMatchHorizontalDown(reference, i, j); + } + + private void checkMatchHorizontalDown(Sweet reference, int i, int j) { + boolean couldBeFourHorizontalMove = false; + if (sameColorUnder(reference, i, j)) { + if (sameColorUnder(reference, i + 1, j)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + couldBeFourHorizontalMove = true; + } else if (couldBeAFiveSpecialMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } else if (bombTwoHorizontal && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.RIGHT) { + moves.get(moves.size() - 1).addToScore(1); + } + } + if (sameColorAbove(reference, i, j)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + } else if (couldBeFourHorizontalMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(2); + } else if (bombTwoHorizontal && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.RIGHT) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + } + + private void checkMatchHorizontalUp(Sweet reference, int i, int j) { + boolean couldBeFourHorizontalMove = false; + couldBeAFiveSpecialMove = false; + if (sameColorAbove(reference, i, j)) { + if (sameColorAbove(reference, i - 1, j)) { + if (!moveAlreadyAdded && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + couldBeFourHorizontalMove = true; + couldBeAFiveSpecialMove = true; + } else if (bombTwoVertical && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(4); + } + if (symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.RIGHT) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + if (sameColorUnder(reference, i, j)) { + if (couldBeFourHorizontalMove && moveAlreadyAdded) { + moves.get(moves.size() - 1).addToScore(2); + } + } + } + + private void checkMatchLeftLeft(Sweet reference, int i, int j) { + if (sameColorOnYourLeft(reference, i, j)) { + if (sameColorOnYourLeft(reference, i, j - 1) && !symmetricalMoveDone) { + addThisMove(reference, grid.get(i).get(j)); + bombTwoHorizontal = true; + } + if (sameColorOnYourLeft(reference, i, j - 1) && symmetricalMoveDone && !moveAlreadyAdded && symmetricalDirection == Move.Direction.RIGHT) { + moves.get(moves.size() - 1).addToScore(1); + } + } + } + + private void checkDirectionRight(Sweet reference, int i, int j) { + initializeBooleansToFindSpecialMoves(); + checkMatchRightRight(reference, i, j); + checkMatchHorizontalUp(reference, i, j); + checkMatchHorizontalDown(reference, i, j); + } + + private void checkMatchRightRight(Sweet reference, int i, int j) { + if (sameColorOnYourRight(reference, i, j)) { + if (sameColorOnYourRight(reference, i, j + 1)) { + addThisMove(reference, grid.get(i).get(j)); + bombTwoHorizontal = true; + } + } + } + + private void addThisMove(Sweet reference, Sweet other) { + if (reference == null || other == null || reference.getType() == other.getType()) { + return; + } + Move move = new Move(reference, other, 1); //score : added later + moves.add(move); + moveAlreadyAdded = true; + symmetricalMoveDone = true; + symmetricalDirection = move.findDirection(); + } + + private boolean canGoUp(int i, int j) { + return i != 0 && grid.get(i - 1) != null && j < grid.get(i - 1).size() && grid.get(i - 1).get(j) != null; + } + + private boolean canGoDown(int i, int j) { + return i < grid.size() - 1 && grid.get(i + 1) != null && j < grid.get(i + 1).size() && grid.get(i + 1).get(j) != null; + } + + private boolean canGoLeft(int i, int j) { + return j != 0 && j - 1 < grid.get(i).size() && grid.get(i).get(j - 1) != null; + } + + private boolean canGoRight(int i, int j) { + return j < grid.get(i).size() - 1 && grid.get(i).get(j + 1) != null; + } + + private boolean sameColor(Sweet reference, Sweet other) { + if (reference == null || other == null) { + return false; + } + return reference.getType() == other.getType(); + } + + private boolean sameColorOnYourRight(Sweet reference, int i, int j) { + if (canGoRight(i, j)) { + if (sameColor(reference, grid.get(i).get(j + 1))) { + return true; + } + } + return false; + } + + private boolean sameColorOnYourLeft(Sweet reference, int i, int j) { + if (canGoLeft(i, j)) { + if (sameColor(reference, grid.get(i).get(j - 1))) { + return true; + } + } + return false; + } + + private boolean sameColorAbove(Sweet reference, int i, int j) { + if (canGoUp(i, j)) { + if (sameColor(reference, grid.get(i - 1).get(j))) { + return true; + } + } + return false; + } + + private boolean sameColorUnder(Sweet reference, int i, int j) { + if (canGoDown(i, j)) { + if (sameColor(reference, grid.get(i + 1).get(j))) { + return true; + } + } + return false; + } + + private void initializeBooleansToFindSpecialMoves() { + moveAlreadyAdded = false; + couldBeAFiveSpecialMove = false; + bombTwoHorizontal = false; + bombTwoVertical = false; + } + + private Move findBestMove() { + Move bestMove = null; + int bestScore = 0; + for (int i = 0; i < moves.size(); i++) { + if (moves.get(i).getScore() >= bestScore) { + bestMove = moves.get(i); + bestScore = moves.get(i).getScore(); + } + } + return bestMove; + } + + public List getMoves() { + return moves; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/engine/Sweet.java b/app/src/main/java/com/applidium/candycrushsolver/engine/Sweet.java new file mode 100644 index 0000000..43396c0 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/engine/Sweet.java @@ -0,0 +1,83 @@ +package com.applidium.candycrushsolver.engine; + + +import android.support.annotation.ColorInt; + +import org.opencv.core.Point; +import org.opencv.core.Scalar; + +public class Sweet { + + public enum Type { + GREEN("#00FF00FF"), + RED("#0000FFFF"), + ORANGE("#0066CCFF"), + YELLOW("00CCCCFF"), + PURPLE("CC0080FF"), + BLUE("FF0000FF"); + + final Scalar color; + + Type(String color) { + int c = parseRGBA(color); + this.color = new Scalar(red(c), green(c), blue(c), alpha(c)); + } + } + + private final Type color; + + Point position; + + public Sweet(Type type, Point position) { + this.color = type; + this.position = position; + } + + public Sweet(int colorIndex, Point position) { + this(Type.values()[colorIndex], position); + } + + public Type getType() { + return color; + } + + public Point getPosition() { return position; } + + public double getX() { + return position.x; + } + + public double getY() { + return position.y; + } + + public void setPosition(Point position) { + this.position = position; + } + + @Override + public String toString() { + return color.toString(); + } + + @ColorInt + private static int parseRGBA(String color) { + return Integer.parseInt(color.substring(1), 16); + } + + private static int red(@ColorInt int color) { + return (color & 0xFF000000) >> 24; + } + + private static int green(@ColorInt int color) { + return (color & 0x00FF0000) >> 16; + } + + private static int blue(@ColorInt int color) { + return (color & 0x0000FF00) >> 8; + } + + private static int alpha(@ColorInt int color) { + return (color & 0x000000FF) >> 0; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Error.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Error.java new file mode 100644 index 0000000..cd4c729 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Error.java @@ -0,0 +1,5 @@ +package com.applidium.candycrushsolver.monitoring.core; + +public abstract class Error extends RuntimeException { + public abstract int getId(); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Errors.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Errors.java new file mode 100644 index 0000000..ac03d2e --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/Errors.java @@ -0,0 +1,5 @@ +package com.applidium.candycrushsolver.monitoring.core; + +public class Errors { + public static final int GENERIC = 0; +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/UnexpectedError.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/UnexpectedError.java new file mode 100644 index 0000000..9fb6c62 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/core/UnexpectedError.java @@ -0,0 +1,8 @@ +package com.applidium.candycrushsolver.monitoring.core; + +public class UnexpectedError extends Error { + @Override + public int getId() { + return Errors.GENERIC; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/ComponentManager.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/ComponentManager.java new file mode 100644 index 0000000..681b732 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/ComponentManager.java @@ -0,0 +1,91 @@ +package com.applidium.candycrushsolver.monitoring.di; + +import android.content.SharedPreferences; +import android.support.annotation.NonNull; + +import com.applidium.candycrushsolver.monitoring.di.common.ApplicationComponent; +import com.applidium.candycrushsolver.monitoring.di.common.DaggerApplicationComponent; +import com.applidium.candycrushsolver.monitoring.di.common.PreferencesModule; +import com.applidium.candycrushsolver.monitoring.di.crashes.CrashesComponent; +import com.applidium.candycrushsolver.monitoring.di.crashes.CrashesModule; +import com.applidium.candycrushsolver.monitoring.di.logging.LoggingComponent; +import com.applidium.candycrushsolver.monitoring.di.logging.LoggingModule; +import com.applidium.candycrushsolver.monitoring.di.threading.ThreadingComponent; +import com.applidium.candycrushsolver.monitoring.di.threading.ThreadingModule; +import com.applidium.candycrushsolver.monitoring.di.trace.TracerModule; + +import java.io.File; + +public class ComponentManager { + + protected static ApplicationComponent applicationComponent; + protected static LoggingComponent loggingComponent; + protected static ThreadingComponent threadingComponent; + protected static CrashesComponent crashesComponent; + + public static void init(SharedPreferences preferences, File cacheDirectory) { + LoggingModule loggingModule = new LoggingModule(); + PreferencesModule preferencesModule = new PreferencesModule(preferences); + TracerModule tracerModule = new TracerModule(); + initApplicationComponent(loggingModule, preferencesModule, tracerModule); + initLoggingComponent(); + ThreadingModule threadingModule = new ThreadingModule(); + initThreadingComponent(threadingModule); + CrashesModule crashesModule = new CrashesModule(); + initCrashesComponent(crashesModule); + } + + protected static void initApplicationComponent( + LoggingModule loggingModule, + PreferencesModule preferencesModule, + TracerModule tracerModule + ) { + applicationComponent = DaggerApplicationComponent + .builder() + .loggingModule(loggingModule) + .tracerModule(tracerModule) + .preferencesModule(preferencesModule) + .build(); + } + + protected static void initLoggingComponent() { + loggingComponent = applicationComponent.loggingComponentBuilder().build(); + } + + protected static void initThreadingComponent(ThreadingModule threadingModule) { + threadingComponent = applicationComponent.plus(threadingModule); + } + + protected static void initCrashesComponent(CrashesModule crashesModule) { + crashesComponent = applicationComponent.plus(crashesModule); + } + + public static ApplicationComponent getApplicationComponent() { + return safeReturn(applicationComponent); + } + + public static LoggingComponent getLoggingComponent() { + return safeReturn(loggingComponent); + } + + public static ThreadingComponent getThreadingComponent() { + return safeReturn(threadingComponent); + } + + public static CrashesComponent getCrashesComponent() { + return safeReturn(crashesComponent); + } + + @NonNull + private static C safeReturn(C component) { + if (component == null) { + fail(); + } + return component; + } + + private static void fail() { + String message = "ComponentManager.init() was not called on Application#onCreate()"; + throw new RuntimeException(message); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/ApplicationComponent.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/ApplicationComponent.java new file mode 100644 index 0000000..04b145c --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/ApplicationComponent.java @@ -0,0 +1,28 @@ +package com.applidium.candycrushsolver.monitoring.di.common; + +import com.applidium.candycrushsolver.monitoring.di.crashes.CrashesModule; +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; +import com.applidium.candycrushsolver.monitoring.di.logging.LoggingComponent; +import com.applidium.candycrushsolver.monitoring.di.logging.LoggingModule; +import com.applidium.candycrushsolver.monitoring.di.threading.ThreadingComponent; +import com.applidium.candycrushsolver.monitoring.di.threading.ThreadingModule; +import com.applidium.candycrushsolver.monitoring.di.trace.TracerModule; +import com.applidium.candycrushsolver.monitoring.di.crashes.CrashesComponent; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + LoggingModule.class, + PreferencesModule.class, + TracerModule.class +}) +public interface ApplicationComponent { + Logger logger(); + + LoggingComponent.Builder loggingComponentBuilder(); + CrashesComponent plus(CrashesModule module); + ThreadingComponent plus(ThreadingModule module); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/PreferencesModule.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/PreferencesModule.java new file mode 100644 index 0000000..a049598 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/common/PreferencesModule.java @@ -0,0 +1,23 @@ +package com.applidium.candycrushsolver.monitoring.di.common; + +import android.content.SharedPreferences; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class PreferencesModule { + + private final SharedPreferences preferences; + + public PreferencesModule(SharedPreferences preferences) { + this.preferences = preferences; + } + + @Provides @Singleton + SharedPreferences preferences() { + return preferences; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesComponent.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesComponent.java new file mode 100644 index 0000000..82222e8 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesComponent.java @@ -0,0 +1,24 @@ +package com.applidium.candycrushsolver.monitoring.di.crashes; + +import android.app.Application; +import android.content.ComponentCallbacks2; + +import com.applidium.candycrushsolver.monitoring.utils.aspect.RethrowUnexpectedAspect; + +import net.hockeyapp.android.CrashManagerListener; + +import javax.inject.Named; + +import dagger.Subcomponent; + +@Subcomponent(modules = { + CrashesModule.class +}) +public interface CrashesComponent { + @Named("crash") + CrashManagerListener crashesListener(); + Application.ActivityLifecycleCallbacks activityListener(); + ComponentCallbacks2 componentListener(); + + void inject(RethrowUnexpectedAspect rethrowUnexpectedAspect); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesModule.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesModule.java new file mode 100644 index 0000000..d47d85c --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/crashes/CrashesModule.java @@ -0,0 +1,56 @@ +package com.applidium.candycrushsolver.monitoring.di.crashes; + +import android.app.Application; +import android.content.ComponentCallbacks2; +import android.support.annotation.NonNull; + +import com.applidium.candycrushsolver.monitoring.utils.crashes.HockeyAppCrashManagerListener; + +import net.hockeyapp.android.CrashManagerListener; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; + +@Module +public class CrashesModule { + + private static final String RETHROWN_MESSAGE = "Error has been rethrown. App did not crash.\n\n"; + + @Provides @Named("rethrown") + CrashManagerListener provideRethrownListener( + final HockeyAppCrashManagerListener hockeyAppListener + ) { + return getRethrownCrashManagerListener(hockeyAppListener); + } + + @NonNull + private CrashManagerListener getRethrownCrashManagerListener( + final HockeyAppCrashManagerListener hockeyAppListener + ) { + return new CrashManagerListener() { + @Override + public String getDescription() { + return RETHROWN_MESSAGE + hockeyAppListener.getDescription(); + } + }; + } + + @Provides @Named("crash") + CrashManagerListener provideCrashListener(HockeyAppCrashManagerListener hockeyAppListener) { + return hockeyAppListener; + } + + @Provides + Application.ActivityLifecycleCallbacks provideActivityListener( + HockeyAppCrashManagerListener hockeyAppListener + ) { + return hockeyAppListener; + } + + @Provides + ComponentCallbacks2 provideComponentListener(HockeyAppCrashManagerListener hockeyAppListener) { + return hockeyAppListener; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingComponent.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingComponent.java new file mode 100644 index 0000000..a2cbf16 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingComponent.java @@ -0,0 +1,29 @@ +package com.applidium.candycrushsolver.monitoring.di.logging; + +import com.applidium.candycrushsolver.android.BusinessService; +import com.applidium.candycrushsolver.android.HeadService; +import com.applidium.candycrushsolver.android.MainActivity; +import com.applidium.candycrushsolver.android.SettingsFragment; +import com.applidium.candycrushsolver.android.TutoActivity; +import com.applidium.candycrushsolver.monitoring.utils.aspect.TracerAspect; + +import javax.inject.Singleton; + +import dagger.Subcomponent; + +@Singleton +@Subcomponent +public interface LoggingComponent { + void inject(MainActivity injected); + void inject(SettingsFragment injected); + void inject(TutoActivity injected); + void inject(BusinessService injected); + void inject(HeadService injected); + + void inject(TracerAspect tracerAspect); + + @Subcomponent.Builder + interface Builder { + LoggingComponent build(); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingModule.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingModule.java new file mode 100644 index 0000000..e6867b4 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/logging/LoggingModule.java @@ -0,0 +1,31 @@ +package com.applidium.candycrushsolver.monitoring.di.logging; + +import com.applidium.candycrushsolver.BuildConfig; +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.monitoring.utils.logging.NoOpLogger; +import com.applidium.candycrushsolver.monitoring.utils.logging.TimberLogger; +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class LoggingModule { + + protected static final boolean SHOW_HASH = Settings.logging.show_hashs; + protected static final boolean ENABLED = Settings.logging.enabled; + + @Provides @Singleton + Logger provideLogger() { + return getLogger(); + } + + protected Logger getLogger() { + if (BuildConfig.DEBUG && ENABLED) { + return new TimberLogger(SHOW_HASH); + } + return new NoOpLogger(); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingComponent.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingComponent.java new file mode 100644 index 0000000..a7378a0 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingComponent.java @@ -0,0 +1,13 @@ +package com.applidium.candycrushsolver.monitoring.di.threading; + +import com.applidium.candycrushsolver.monitoring.utils.aspect.ThreadingAspect; + +import javax.inject.Singleton; + +import dagger.Subcomponent; + +@Singleton +@Subcomponent(modules = ThreadingModule.class) +public interface ThreadingComponent { + void inject(ThreadingAspect aspect); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingModule.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingModule.java new file mode 100644 index 0000000..f0905f3 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/threading/ThreadingModule.java @@ -0,0 +1,27 @@ +package com.applidium.candycrushsolver.monitoring.di.threading; + +import com.applidium.candycrushsolver.monitoring.utils.threading.UiThread; +import com.applidium.candycrushsolver.monitoring.utils.threading.JobExecutor; +import com.applidium.candycrushsolver.monitoring.utils.threading.PostExecutionThread; +import com.applidium.candycrushsolver.monitoring.utils.threading.ThreadExecutor; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class ThreadingModule { + + @Provides + @Singleton + ThreadExecutor provideExecutor(JobExecutor instance) { + return instance; + } + + @Provides + @Singleton + PostExecutionThread providePostThread(UiThread instance) { + return instance; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/trace/TracerModule.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/trace/TracerModule.java new file mode 100644 index 0000000..77118ee --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/di/trace/TracerModule.java @@ -0,0 +1,56 @@ +package com.applidium.candycrushsolver.monitoring.di.trace; + +import android.support.annotation.NonNull; + +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.monitoring.utils.crashes.HockeyAppCrashManagerListener; +import com.applidium.candycrushsolver.monitoring.utils.trace.LoggerTracer; +import com.applidium.candycrushsolver.monitoring.utils.trace.NoOpTracer; +import com.applidium.candycrushsolver.monitoring.utils.trace.Tracer; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class TracerModule { + + protected static final boolean TRACING = Settings.tracing.enabled; + protected static final boolean LOGGING = Settings.logging.enabled; + protected static final boolean CRASHES = Settings.crashes.enabled; + + @Provides @Singleton + protected Tracer provideTracer( + final HockeyAppCrashManagerListener hockeyAppTracer, + NoOpTracer noOpTracer, + final LoggerTracer loggerTracer + ) { + if (!TRACING) { + return noOpTracer; + } + if (CRASHES && LOGGING) { + return getWrapperTracer(hockeyAppTracer, loggerTracer); + } + if (CRASHES) { + return hockeyAppTracer; + } + if (LOGGING) { + return loggerTracer; + } + return noOpTracer; + } + + @NonNull + private Tracer getWrapperTracer( + final HockeyAppCrashManagerListener hockeyAppTracer, final LoggerTracer loggerTracer + ) { + return new Tracer() { + @Override + public void trace(Object target, String message, Object[] parameterValues) { + hockeyAppTracer.trace(target, message, parameterValues); + loggerTracer.trace(target, message, parameterValues); + } + }; + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/RethrowUnexpectedAspect.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/RethrowUnexpectedAspect.java new file mode 100644 index 0000000..f51ca01 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/RethrowUnexpectedAspect.java @@ -0,0 +1,106 @@ +package com.applidium.candycrushsolver.monitoring.utils.aspect; + +import android.support.annotation.NonNull; +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.monitoring.core.UnexpectedError; +import com.applidium.candycrushsolver.monitoring.di.ComponentManager; +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; + +import net.hockeyapp.android.CrashManagerListener; +import net.hockeyapp.android.ExceptionHandler; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.CodeSignature; + +import javax.inject.Inject; +import javax.inject.Named; + +@Aspect +public class RethrowUnexpectedAspect { + + private static final boolean ENABLED = Settings.errors.rethrow; + + private static final String ANNOTATION_PACKAGE = "com.applidium.candycrushsolver.utils.rethrowunexpected."; + private static final String RETHROW_UNEXPECTED_POINTCUT = "execution(@" + ANNOTATION_PACKAGE + "RethrowUnexpected * *(..))"; + + private static final boolean CRASHES_ENABLED = Settings.crashes.enabled; + + @Inject + Logger logger; + @Inject @Named("rethrown") + CrashManagerListener crashListener; + + public void init() { + ComponentManager.getCrashesComponent().inject(this); + } + + @Pointcut(RETHROW_UNEXPECTED_POINTCUT) + public void rethrowUnexpected() {} + + @Around("rethrowUnexpected()") + public Object rethrow(ProceedingJoinPoint joinPoint) throws Throwable { + if (!ENABLED) { + return joinPoint.proceed(); + } + checkInit(); + log(joinPoint); + return doFailSafe(joinPoint); + } + + private void log(ProceedingJoinPoint joinPoint) { + Object target = joinPoint.getTarget(); + CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); + Object[] parameterValues = joinPoint.getArgs(); + String message = makeMessage(codeSignature); + logger.w(target, message, parameterValues); + } + + private Object doFailSafe(ProceedingJoinPoint joinPoint) { + try { + return joinPoint.proceed(); + } catch (Throwable throwable) { + if (CRASHES_ENABLED) { + ExceptionHandler.saveException(throwable, null, crashListener); + } + logger.e(throwable, null); + throw new UnexpectedError(); + } + } + + @NonNull + private String makeMessage(CodeSignature codeSignature) { + String methodName = codeSignature.getName(); + String[] parameterNames = codeSignature.getParameterNames(); + + String params = makeParamsFormat(parameterNames); + return "rethrow unexpected " + methodName + "(" + params + ")"; + } + + @NonNull + private String makeParamsFormat(String[] parameterNames) { + StringBuilder builder = new StringBuilder(); + for (String parameterName : parameterNames) { + builder.append(parameterName); + builder.append(": %s,"); + } + int index = builder.lastIndexOf(","); + if (index >= 0) { + builder.deleteCharAt(index); + } + return builder.toString(); + } + + private void checkInit() { + if (logger == null) { + fail(); + } + } + + private static void fail() { + String message = "RethrowUnexpectedAspect#init() was not called on Application#onCreate()"; + throw new RuntimeException(message); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/ThreadingAspect.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/ThreadingAspect.java new file mode 100644 index 0000000..9ee907c --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/ThreadingAspect.java @@ -0,0 +1,97 @@ +package com.applidium.candycrushsolver.monitoring.utils.aspect; + +import com.applidium.candycrushsolver.monitoring.core.UnexpectedError; +import com.applidium.candycrushsolver.monitoring.di.ComponentManager; +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; +import com.applidium.candycrushsolver.monitoring.utils.threading.PostExecutionThread; +import com.applidium.candycrushsolver.monitoring.utils.threading.ThreadExecutor; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +import javax.inject.Inject; + +@Aspect +public class ThreadingAspect { + + private static final String ANNOTATION_PACKAGE = "com.applidium.candycrushsolver.utils.threading."; + public static final String EXECUTION_PREFIX = "execution(@" + ANNOTATION_PACKAGE; + public static final String EXECUTION_SUFFIX = " * *(..))"; + + private static final String EXECUTION_THREAD_POINTCUT = + EXECUTION_PREFIX + "RunOnExecutionThread" + EXECUTION_SUFFIX; + + private static final String POST_EXECUTION_THREAD_POINTCUT = + EXECUTION_PREFIX + "RunOnPostExecutionThread" + EXECUTION_SUFFIX; + + @Inject + ThreadExecutor threadExecutor; + @Inject + PostExecutionThread postExecutionThread; + @Inject + Logger logger; + + public void init() { + ComponentManager.getThreadingComponent().inject(this); + } + + @Pointcut(EXECUTION_THREAD_POINTCUT) + public void executionThreadAnnotated() {} + + @Pointcut(POST_EXECUTION_THREAD_POINTCUT) + public void postExecutionThreadAnnotated() {} + + @Around("executionThreadAnnotated()") + public void runOnExecutionThread(ProceedingJoinPoint joinPoint) { + checkInit(); + logger.v(this, "Running execution joint point " + joinPoint); + threadExecutor.execute(makeExecutorRunnable(joinPoint)); + } + + private Runnable makeExecutorRunnable(final ProceedingJoinPoint joinPoint) { + return new Runnable() { + @Override + public void run() { + proceed(joinPoint); + } + }; + } + + @Around("postExecutionThreadAnnotated()") + public void runOnPostExecutionThread(ProceedingJoinPoint joinPoint) { + checkInit(); + logger.v(this, "Running post execution joint point " + joinPoint); + postExecutionThread.post(makePostRunnable(joinPoint)); + } + + private Runnable makePostRunnable(final ProceedingJoinPoint joinPoint) { + return new Runnable() { + @Override + public void run() { + proceed(joinPoint); + } + }; + } + + private void proceed(ProceedingJoinPoint joinPoint) { + try { + joinPoint.proceed(); + } catch (Throwable throwable) { + logger.e(this, throwable, "Error while changing threads."); + throw new UnexpectedError(); + } + } + + private void checkInit() { + if (logger == null || threadExecutor == null || postExecutionThread == null) { + fail(); + } + } + + private static void fail() { + String message = "ThreadingAspect#init() was not called on Application#onCreate()"; + throw new RuntimeException(message); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/TracerAspect.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/TracerAspect.java new file mode 100644 index 0000000..b4f5286 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/aspect/TracerAspect.java @@ -0,0 +1,83 @@ +package com.applidium.candycrushsolver.monitoring.utils.aspect; + +import android.support.annotation.NonNull; +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.monitoring.di.ComponentManager; +import com.applidium.candycrushsolver.monitoring.utils.trace.Tracer; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.CodeSignature; + +import javax.inject.Inject; + +@Aspect +public class TracerAspect { + + private static final String ANNOTATION_PACKAGE = "com.candycrushsolver.utils.trace."; + private static final String TRACE_POINTCUT = "execution(@" + ANNOTATION_PACKAGE + "Trace * *(..))"; + + private static final Boolean ENABLED = Settings.tracing.enabled; + + @Inject + Tracer tracer; + + public void init() { + ComponentManager.getLoggingComponent().inject(this); + } + + @Pointcut(TRACE_POINTCUT) + public void traceAnnontated() {} + + @Before("traceAnnontated()") + public void trace(JoinPoint joinPoint) { + if (!ENABLED) { + return; + } + checkInit(); + + Object target = joinPoint.getTarget(); + CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); + Object[] parameterValues = joinPoint.getArgs(); + + String message = makeMessage(codeSignature); + + tracer.trace(target, message, parameterValues); + } + + @NonNull + private String makeMessage(CodeSignature codeSignature) { + String methodName = codeSignature.getName(); + String[] parameterNames = codeSignature.getParameterNames(); + + String params = makeParamsFormat(parameterNames); + return methodName + "(" + params + ")"; + } + + @NonNull + private String makeParamsFormat(String[] parameterNames) { + StringBuilder builder = new StringBuilder(); + for (String parameterName : parameterNames) { + builder.append(parameterName); + builder.append(": %s,"); + } + int index = builder.lastIndexOf(","); + if (index >= 0) { + builder.deleteCharAt(index); + } + return builder.toString(); + } + + private void checkInit() { + if (tracer == null) { + fail(); + } + } + + private static void fail() { + String message = "TracerAspect#init() was not called on Application#onCreate()"; + throw new RuntimeException(message); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/crashes/HockeyAppCrashManagerListener.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/crashes/HockeyAppCrashManagerListener.java new file mode 100644 index 0000000..f093fa3 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/crashes/HockeyAppCrashManagerListener.java @@ -0,0 +1,310 @@ +package com.applidium.candycrushsolver.monitoring.utils.crashes; + +import android.app.Activity; +import android.app.Application; +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.FragmentManager; +import android.view.View; +import android.view.ViewGroup; + +import com.applidium.candycrushsolver.Settings; +import com.applidium.candycrushsolver.android.MainActivity; +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; +import com.applidium.candycrushsolver.monitoring.utils.trace.Tracer; + +import net.hockeyapp.android.CrashManagerListener; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.ref.WeakReference; +import java.util.LinkedList; +import java.util.Locale; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class HockeyAppCrashManagerListener extends CrashManagerListener implements + Application.ActivityLifecycleCallbacks, + Tracer, + ComponentCallbacks2 +{ + private final static int MAX_TRACE_LOGS = Settings.crashes.max_trace; + private final static int MAX_COMPONENT_LOGS = Settings.crashes.max_component; + + private static final boolean ENABLED = Settings.crashes.enabled; + private static final boolean ADDITIONAL_DATA = Settings.crashes.additional_data; + + private LinkedList activityStack = new LinkedList<>(); + private WeakReference mostRecentActivity; + + private LinkedList networkLogs = new LinkedList<>(); + + private LinkedList traceLogs = new LinkedList<>(); + + private LinkedList componentLogs = new LinkedList<>(); + + @Inject + HockeyAppCrashManagerListener() {} + + @Override + public boolean shouldAutoUploadCrashes() { + return ENABLED; + } + + @Override + public String getDescription() { + if (!ADDITIONAL_DATA) { + return null; + } + + String stack = getStackDescription(); + String view = getViewDescription(); + String network = getNetworkDescription(); + String trace = getTraceDescription(); + String component = getComponentDescription(); + + return stack + view + network + trace + component; + } + + private String getComponentDescription() { + return formatLogs(componentLogs, "COMPONENT LOGS"); + } + + @NonNull + private String formatLogs(LinkedList logs, String title) { + StringBuilder sb = new StringBuilder(); + + sb.append(title); + sb.append("\n"); + + String underline = new String(new char[title.length()]).replace('\0', '='); + sb.append(underline); + sb.append("\n\n"); + + for (String s : logs) { + sb.append(s).append("\n"); + } + + sb.append("\n\n"); + return sb.toString(); + } + + private String getTraceDescription() { + return formatLogs(traceLogs, "TRACE LOGS"); + } + + private String getStackDescription() { + return formatLogs(activityStack, "ACTIVITY STACK"); + } + + private String getViewDescription() { + StringBuilder sb = new StringBuilder(); + sb.append("VIEW DUMP\n"); + sb.append("=========\n\n"); + + if (mostRecentActivity != null) { + Activity activity = mostRecentActivity.get(); + if (activity != null) { + View content = activity.findViewById(android.R.id.content); + appendViewHierarchy(sb, "", content); + } else { + sb.append("Unable to dump view hierarchy\n"); + } + } + + sb.append("\n\n"); + return sb.toString(); + } + + private static void appendViewHierarchy(StringBuilder sb, String prefix, View view) { + String desc = getPrintableView(prefix, view); + sb.append(desc).append("\n"); + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + prefix = deepenPrefix(prefix); + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View v = viewGroup.getChildAt(i); + appendViewHierarchy(sb, prefix, v); + } + } + } + + @NonNull + private static String deepenPrefix(String prefix) { + if (prefix.length() == 0) { + prefix = "| " + prefix; + } else { + prefix = " " + prefix; + } + return prefix; + } + + @NonNull + private static String getPrintableView(String prefix, View view) { + return String.format(Locale.getDefault(), + "%s%s (%.0f, %.0f, %d, %d) %s", + prefix, + view.getClass().getSimpleName(), + view.getX(), + view.getY(), + view.getWidth(), + view.getHeight(), + getPrintableId(view)); + } + + private static String getPrintableId(View view) { + return view.getId() != -1 ? view.getResources().getResourceName(view.getId()) : ""; + } + + + private String getNetworkDescription() { + return formatLogs(networkLogs, "NETWORK LOGS"); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + String desc = getActivityDesc(activity); + activityStack.add(desc); + } + + private String getActivityDesc(Activity activity) { + String desc = getObjectDescription(activity); + + if (activity instanceof MainActivity) { + FragmentManager fragmentManager = ((MainActivity) activity).getSupportFragmentManager(); + int backstackCount = fragmentManager.getBackStackEntryCount(); + for (int i = 0; i < backstackCount; i++) { + desc += "\n " + fragmentManager.getBackStackEntryAt(i).getName(); + } + } + + return desc; + } + + private static String getObjectDescription(Object object) { + int hash = System.identityHashCode(object); + return String.format( + Locale.getDefault(), + "%s@%X", + object.getClass().getSimpleName(), + hash + ); + } + + @Override + public void onActivityStarted(Activity activity) { + // no-op + } + + @Override + public void onActivityResumed(Activity activity) { + mostRecentActivity = new WeakReference<>(activity); + } + + @Override + public void onActivityPaused(Activity activity) { + // no-op + } + + @Override + public void onActivityStopped(Activity activity) { + // no-op + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + // no-op + } + + @Override + public void onActivityDestroyed(Activity activity) { + activityStack.pop(); + } + + @Override + public void trace(Object target, String message, Object[] parameterValues) { + if (traceLogs.size() == MAX_TRACE_LOGS) { + traceLogs.removeFirst(); + } + String trace = getTraceMessage(target, message, parameterValues); + traceLogs.add(trace); + } + + @NonNull + private String getTraceMessage(Object target, String message, Object[] parameterValues) { + StringBuilder traceBuilder = new StringBuilder(); + if (target != null) { + traceBuilder.append(getObjectDescription(target)); + traceBuilder.append(" | "); + } + if (parameterValues != null && parameterValues.length > 0) { + traceBuilder.append(String.format(message, parameterValues)); + } else { + traceBuilder.append(message); + } + return traceBuilder.toString(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + String message = "onConfigurationChanged " + newConfig; + addComponentMessage(message); + } + + @Override + public void onLowMemory() { + addComponentMessage("onLowMemory"); + } + + @Override + public void onTrimMemory(int level) { + addComponentMessage("onTrimMemory " + level); + } + + private void addComponentMessage(String message) { + if (componentLogs.size() == MAX_COMPONENT_LOGS) { + componentLogs.removeFirst(); + } + componentLogs.add(message); + } + + public static class LoggerTracer implements Tracer { + + private final Logger logger; + + @Inject + LoggerTracer(Logger logger) { + this.logger = logger; + } + + @Override + public void trace(Object target, String message, Object[] parameterValues) { + logger.i(target, message, parameterValues); + } + } + + public static class NoOpTracer implements Tracer { + + @Inject + NoOpTracer() {} + + @Override + public void trace(Object target, String message, Object[] parameterValues) { + } + } + + @Retention(RetentionPolicy.CLASS) + @Target({ ElementType.METHOD }) + public static @interface Trace { + } + + public static interface Tracer { + void trace(Object target, String message, Object[] parameterValues); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/Logger.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/Logger.java new file mode 100644 index 0000000..7e1ef0f --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/Logger.java @@ -0,0 +1,29 @@ +package com.applidium.candycrushsolver.monitoring.utils.logging; + +public interface Logger { + + void v(String message, Object... args); + void v(Object instance, String message, Object... args); + void v(Throwable t, String message, Object... args); + void v(Object instance, Throwable t, String message, Object... args); + + void d(String message, Object... args); + void d(Object instance, String message, Object... args); + void d(Throwable t, String message, Object... args); + void d(Object instance, Throwable t, String message, Object... args); + + void i(String message, Object... args); + void i(Object instance, String message, Object... args); + void i(Throwable t, String message, Object... args); + void i(Object instance, Throwable t, String message, Object... args); + + void w(String message, Object... args); + void w(Object instance, String message, Object... args); + void w(Throwable t, String message, Object... args); + void w(Object instance, Throwable t, String message, Object... args); + + void e(String message, Object... args); + void e(Object instance, String message, Object... args); + void e(Throwable t, String message, Object... args); + void e(Object instance, Throwable t, String message, Object... args); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/LoggerComponentCallbacks2.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/LoggerComponentCallbacks2.java new file mode 100644 index 0000000..bd04fec --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/LoggerComponentCallbacks2.java @@ -0,0 +1,31 @@ +package com.applidium.candycrushsolver.monitoring.utils.logging; + +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; + +import javax.inject.Singleton; + +@Singleton +public class LoggerComponentCallbacks2 implements ComponentCallbacks2 { + + private final Logger logger; + + public LoggerComponentCallbacks2(Logger logger) { + this.logger = logger; + } + + @Override + public void onTrimMemory(int level) { + logger.w("onTrimMemory(level: %s)", level); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + logger.i("onConfigurationChanged(newConfig: %s)", newConfig); + } + + @Override + public void onLowMemory() { + logger.w("onLowMemory()"); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/NoOpLogger.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/NoOpLogger.java new file mode 100644 index 0000000..cbac94c --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/NoOpLogger.java @@ -0,0 +1,103 @@ +package com.applidium.candycrushsolver.monitoring.utils.logging; + +public class NoOpLogger implements Logger { + @Override + public void v(String message, Object... args) { + // no-op + } + + @Override + public void v(Object instance, String message, Object... args) { + // no-op + } + + @Override + public void v(Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void v(Object instance, Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void d(String message, Object... args) { + // no-op + } + + @Override + public void d(Object instance, String message, Object... args) { + // no-op + } + + @Override + public void d(Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void d(Object instance, Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void i(String message, Object... args) { + // no-op + } + + @Override + public void i(Object instance, String message, Object... args) { + // no-op + } + + @Override + public void i(Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void i(Object instance, Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void w(String message, Object... args) { + // no-op + } + + @Override + public void w(Object instance, String message, Object... args) { + // no-op + } + + @Override + public void w(Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void w(Object instance, Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void e(String message, Object... args) { + // no-op + } + + @Override + public void e(Object instance, String message, Object... args) { + // no-op + } + + @Override + public void e(Throwable t, String message, Object... args) { + // no-op + } + + @Override + public void e(Object instance, Throwable t, String message, Object... args) { + // no-op + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/TimberLogger.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/TimberLogger.java new file mode 100644 index 0000000..e06ae67 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/logging/TimberLogger.java @@ -0,0 +1,148 @@ +package com.applidium.candycrushsolver.monitoring.utils.logging; + +import timber.log.Timber; + +public final class TimberLogger implements Logger { + + private boolean showHash = true; + + public TimberLogger() { + init(); + } + + public TimberLogger(boolean showHash) { + this.showHash = showHash; + init(); + } + + private void init() { + Timber.v("init()"); + } + + @Override + protected void finalize() throws Throwable { + Timber.v("finalize()"); + super.finalize(); + } + + public void setShowHash(boolean showHash) { + this.showHash = showHash; + } + + private String getTag(Object instance) { + if (instance == null) { + return ""; + } + String name = instance.getClass().getSimpleName(); + String hash = "@" + System.identityHashCode(instance); + if (showHash) { + return name + hash; + } + return name; + } + + /** VERBOSE */ + @Override + public void v(String message, Object... args) { + Timber.v(message, args); + } + + @Override + public void v(Object instance, String message, Object... args) { + Timber.tag(getTag(instance)).v(message, args); + } + + @Override + public void v(Throwable t, String message, Object... args) { + Timber.v(t, message, args); + } + + @Override + public void v(Object instance, Throwable t, String message, Object... args) { + Timber.tag(getTag(instance)).v(t, message, args); + } + + /** DEBUG */ + @Override + public void d(String message, Object... args) { + Timber.d(message, args); + } + + @Override + public void d(Object instance, String message, Object... args) { + Timber.tag(getTag(instance)).d(message, args); + } + + @Override + public void d(Throwable t, String message, Object... args) { + Timber.d(t, message, args); + } + + @Override + public void d(Object instance, Throwable t, String message, Object... args) { + Timber.tag(getTag(instance)).d(t, message, args); + } + + /** INFO */ + @Override + public void i(String message, Object... args) { + Timber.i(message, args); + } + + @Override + public void i(Object instance, String message, Object... args) { + Timber.tag(getTag(instance)).i(message, args); + } + + @Override + public void i(Throwable t, String message, Object... args) { + Timber.i(t, message, args); + } + + @Override + public void i(Object instance, Throwable t, String message, Object... args) { + Timber.tag(getTag(instance)).i(t, message, args); + } + + /** WARNING */ + @Override + public void w(String message, Object... args) { + Timber.w(message, args); + } + + @Override + public void w(Object instance, String message, Object... args) { + Timber.tag(getTag(instance)).w(message, args); + } + + @Override + public void w(Throwable t, String message, Object... args) { + Timber.w(t, message, args); + } + + @Override + public void w(Object instance, Throwable t, String message, Object... args) { + Timber.tag(getTag(instance)).w(t, message, args); + } + + /** ERROR */ + @Override + public void e(String message, Object... args) { + Timber.e(message, args); + } + + @Override + public void e(Object instance, String message, Object... args) { + Timber.tag(getTag(instance)).e(message, args); + } + + @Override + public void e(Throwable t, String message, Object... args) { + Timber.e(t, message, args); + } + + @Override + public void e(Object instance, Throwable t, String message, Object... args) { + Timber.tag(getTag(instance)).e(t, message, args); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/JobExecutor.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/JobExecutor.java new file mode 100644 index 0000000..845b9b9 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/JobExecutor.java @@ -0,0 +1,50 @@ +package com.applidium.candycrushsolver.monitoring.utils.threading; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class JobExecutor implements ThreadExecutor { + + private static final int INITIAL_POOL_SIZE = 3; + private static final int MAX_POOL_SIZE = 5; + + // Sets the amount of time an idle thread waits before terminating + private static final int KEEP_ALIVE_TIME = 10; + + // Sets the Time Unit to seconds + private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; + + private final BlockingQueue workQueue; + private final ThreadPoolExecutor threadPoolExecutor; + private final ThreadFactory threadFactory; + + @Inject + JobExecutor() { + this.workQueue = new LinkedBlockingQueue<>(); + this.threadFactory = Executors.defaultThreadFactory(); + this.threadPoolExecutor = new ThreadPoolExecutor( + INITIAL_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + KEEP_ALIVE_TIME_UNIT, + this.workQueue, + this.threadFactory + ); + } + + @Override + public void execute(Runnable runnable) { + if (runnable == null) { + throw new IllegalArgumentException("Runnable to execute cannot be null"); + } + this.threadPoolExecutor.execute(runnable); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/PostExecutionThread.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/PostExecutionThread.java new file mode 100644 index 0000000..b9c6059 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/PostExecutionThread.java @@ -0,0 +1,5 @@ +package com.applidium.candycrushsolver.monitoring.utils.threading; + +public interface PostExecutionThread { + void post(Runnable runnable); +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/ThreadExecutor.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/ThreadExecutor.java new file mode 100644 index 0000000..36531e4 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/ThreadExecutor.java @@ -0,0 +1,6 @@ +package com.applidium.candycrushsolver.monitoring.utils.threading; + +import java.util.concurrent.Executor; + +public interface ThreadExecutor extends Executor { +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/UiThread.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/UiThread.java new file mode 100644 index 0000000..fcb5d92 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/threading/UiThread.java @@ -0,0 +1,23 @@ +package com.applidium.candycrushsolver.monitoring.utils.threading; + +import android.os.Handler; +import android.os.Looper; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class UiThread implements PostExecutionThread { + + private final Handler handler; + + @Inject + UiThread() { + this.handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void post(Runnable runnable) { + handler.post(runnable); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/LoggerTracer.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/LoggerTracer.java new file mode 100644 index 0000000..22ec61d --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/LoggerTracer.java @@ -0,0 +1,20 @@ +package com.applidium.candycrushsolver.monitoring.utils.trace; + +import com.applidium.candycrushsolver.monitoring.utils.logging.Logger; + +import javax.inject.Inject; + +public class LoggerTracer implements Tracer { + + private final Logger logger; + + @Inject + LoggerTracer(Logger logger) { + this.logger = logger; + } + + @Override + public void trace(Object target, String message, Object[] parameterValues) { + logger.i(target, message, parameterValues); + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/NoOpTracer.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/NoOpTracer.java new file mode 100644 index 0000000..562bf96 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/NoOpTracer.java @@ -0,0 +1,13 @@ +package com.applidium.candycrushsolver.monitoring.utils.trace; + +import javax.inject.Inject; + +public class NoOpTracer implements Tracer { + + @Inject + NoOpTracer() {} + + @Override + public void trace(Object target, String message, Object[] parameterValues) { + } +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Trace.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Trace.java new file mode 100644 index 0000000..cd50ff4 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Trace.java @@ -0,0 +1,11 @@ +package com.applidium.candycrushsolver.monitoring.utils.trace; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD }) +public @interface Trace { +} diff --git a/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Tracer.java b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Tracer.java new file mode 100644 index 0000000..0dafff4 --- /dev/null +++ b/app/src/main/java/com/applidium/candycrushsolver/monitoring/utils/trace/Tracer.java @@ -0,0 +1,5 @@ +package com.applidium.candycrushsolver.monitoring.utils.trace; + +public interface Tracer { + void trace(Object target, String message, Object[] parameterValues); +} diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..5654918 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_wb.png b/app/src/main/res/drawable-hdpi/ic_launcher_wb.png new file mode 100644 index 0000000..8da02e9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_wb.png differ diff --git a/app/src/main/res/drawable-hdpi/infinite.png b/app/src/main/res/drawable-hdpi/infinite.png new file mode 100644 index 0000000..7ebdaeb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/infinite.png differ diff --git a/app/src/main/res/drawable-hdpi/notif_no.png b/app/src/main/res/drawable-hdpi/notif_no.png new file mode 100644 index 0000000..94507df Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notif_no.png differ diff --git a/app/src/main/res/drawable-hdpi/notif_yes.png b/app/src/main/res/drawable-hdpi/notif_yes.png new file mode 100644 index 0000000..9cdd53f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notif_yes.png differ diff --git a/app/src/main/res/drawable-hdpi/one.png b/app/src/main/res/drawable-hdpi/one.png new file mode 100644 index 0000000..93ce52f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/one.png differ diff --git a/app/src/main/res/drawable-hdpi/one_red.png b/app/src/main/res/drawable-hdpi/one_red.png new file mode 100644 index 0000000..3871651 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/one_red.png differ diff --git a/app/src/main/res/drawable-hdpi/question.png b/app/src/main/res/drawable-hdpi/question.png new file mode 100644 index 0000000..a72c4e1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/question.png differ diff --git a/app/src/main/res/drawable-hdpi/star.png b/app/src/main/res/drawable-hdpi/star.png new file mode 100644 index 0000000..1ba8f86 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/star.png differ diff --git a/app/src/main/res/drawable-hdpi/three.png b/app/src/main/res/drawable-hdpi/three.png new file mode 100644 index 0000000..ed39e8b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/three.png differ diff --git a/app/src/main/res/drawable-hdpi/three_red.png b/app/src/main/res/drawable-hdpi/three_red.png new file mode 100644 index 0000000..72b615c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/three_red.png differ diff --git a/app/src/main/res/drawable-hdpi/tuto1.png b/app/src/main/res/drawable-hdpi/tuto1.png new file mode 100644 index 0000000..a9cd4ed Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tuto1.png differ diff --git a/app/src/main/res/drawable-hdpi/tuto2.png b/app/src/main/res/drawable-hdpi/tuto2.png new file mode 100644 index 0000000..2a408b6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tuto2.png differ diff --git a/app/src/main/res/drawable-hdpi/tuto3.png b/app/src/main/res/drawable-hdpi/tuto3.png new file mode 100644 index 0000000..b9e19cf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tuto3.png differ diff --git a/app/src/main/res/drawable-hdpi/two.png b/app/src/main/res/drawable-hdpi/two.png new file mode 100644 index 0000000..48a746f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/two.png differ diff --git a/app/src/main/res/drawable-hdpi/two_red.png b/app/src/main/res/drawable-hdpi/two_red.png new file mode 100644 index 0000000..5d4ad2d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/two_red.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..897555f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_wb.png b/app/src/main/res/drawable-mdpi/ic_launcher_wb.png new file mode 100644 index 0000000..d87ee9d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_wb.png differ diff --git a/app/src/main/res/drawable-mdpi/infinite.png b/app/src/main/res/drawable-mdpi/infinite.png new file mode 100644 index 0000000..706ea5b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/infinite.png differ diff --git a/app/src/main/res/drawable-mdpi/notif_no.png b/app/src/main/res/drawable-mdpi/notif_no.png new file mode 100644 index 0000000..0c46bc0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notif_no.png differ diff --git a/app/src/main/res/drawable-mdpi/notif_yes.png b/app/src/main/res/drawable-mdpi/notif_yes.png new file mode 100644 index 0000000..2a7585a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notif_yes.png differ diff --git a/app/src/main/res/drawable-mdpi/one.png b/app/src/main/res/drawable-mdpi/one.png new file mode 100644 index 0000000..50724c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/one.png differ diff --git a/app/src/main/res/drawable-mdpi/one_red.png b/app/src/main/res/drawable-mdpi/one_red.png new file mode 100644 index 0000000..782bc1c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/one_red.png differ diff --git a/app/src/main/res/drawable-mdpi/question.png b/app/src/main/res/drawable-mdpi/question.png new file mode 100644 index 0000000..02f6178 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/question.png differ diff --git a/app/src/main/res/drawable-mdpi/star.png b/app/src/main/res/drawable-mdpi/star.png new file mode 100644 index 0000000..a0cd776 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/star.png differ diff --git a/app/src/main/res/drawable-mdpi/three.png b/app/src/main/res/drawable-mdpi/three.png new file mode 100644 index 0000000..3ff940a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/three.png differ diff --git a/app/src/main/res/drawable-mdpi/three_red.png b/app/src/main/res/drawable-mdpi/three_red.png new file mode 100644 index 0000000..dd619c5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/three_red.png differ diff --git a/app/src/main/res/drawable-mdpi/tuto1.png b/app/src/main/res/drawable-mdpi/tuto1.png new file mode 100644 index 0000000..e724d69 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tuto1.png differ diff --git a/app/src/main/res/drawable-mdpi/tuto2.png b/app/src/main/res/drawable-mdpi/tuto2.png new file mode 100644 index 0000000..06749ea Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tuto2.png differ diff --git a/app/src/main/res/drawable-mdpi/tuto3.png b/app/src/main/res/drawable-mdpi/tuto3.png new file mode 100644 index 0000000..5887a37 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tuto3.png differ diff --git a/app/src/main/res/drawable-mdpi/two.png b/app/src/main/res/drawable-mdpi/two.png new file mode 100644 index 0000000..172709a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/two.png differ diff --git a/app/src/main/res/drawable-mdpi/two_red.png b/app/src/main/res/drawable-mdpi/two_red.png new file mode 100644 index 0000000..1728c30 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/two_red.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..cf4e467 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_wb.png b/app/src/main/res/drawable-xhdpi/ic_launcher_wb.png new file mode 100644 index 0000000..975e87e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_wb.png differ diff --git a/app/src/main/res/drawable-xhdpi/infinite.png b/app/src/main/res/drawable-xhdpi/infinite.png new file mode 100644 index 0000000..f84eed4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/infinite.png differ diff --git a/app/src/main/res/drawable-xhdpi/notif_no.png b/app/src/main/res/drawable-xhdpi/notif_no.png new file mode 100644 index 0000000..c8ef1fb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notif_no.png differ diff --git a/app/src/main/res/drawable-xhdpi/notif_yes.png b/app/src/main/res/drawable-xhdpi/notif_yes.png new file mode 100644 index 0000000..0968134 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notif_yes.png differ diff --git a/app/src/main/res/drawable-xhdpi/one.png b/app/src/main/res/drawable-xhdpi/one.png new file mode 100644 index 0000000..06e2e57 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/one.png differ diff --git a/app/src/main/res/drawable-xhdpi/one_red.png b/app/src/main/res/drawable-xhdpi/one_red.png new file mode 100644 index 0000000..9f9938d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/one_red.png differ diff --git a/app/src/main/res/drawable-xhdpi/question.png b/app/src/main/res/drawable-xhdpi/question.png new file mode 100644 index 0000000..a21f56d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/question.png differ diff --git a/app/src/main/res/drawable-xhdpi/star.png b/app/src/main/res/drawable-xhdpi/star.png new file mode 100644 index 0000000..a8dc006 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/star.png differ diff --git a/app/src/main/res/drawable-xhdpi/three.png b/app/src/main/res/drawable-xhdpi/three.png new file mode 100644 index 0000000..7643b12 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/three.png differ diff --git a/app/src/main/res/drawable-xhdpi/three_red.png b/app/src/main/res/drawable-xhdpi/three_red.png new file mode 100644 index 0000000..2375121 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/three_red.png differ diff --git a/app/src/main/res/drawable-xhdpi/tuto1.png b/app/src/main/res/drawable-xhdpi/tuto1.png new file mode 100644 index 0000000..327edcd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tuto1.png differ diff --git a/app/src/main/res/drawable-xhdpi/tuto2.png b/app/src/main/res/drawable-xhdpi/tuto2.png new file mode 100644 index 0000000..5522236 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tuto2.png differ diff --git a/app/src/main/res/drawable-xhdpi/tuto3.png b/app/src/main/res/drawable-xhdpi/tuto3.png new file mode 100644 index 0000000..b13221d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tuto3.png differ diff --git a/app/src/main/res/drawable-xhdpi/two.png b/app/src/main/res/drawable-xhdpi/two.png new file mode 100644 index 0000000..d469800 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/two.png differ diff --git a/app/src/main/res/drawable-xhdpi/two_red.png b/app/src/main/res/drawable-xhdpi/two_red.png new file mode 100644 index 0000000..8bfd02d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/two_red.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b106fd8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_wb.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_wb.png new file mode 100644 index 0000000..7a8de74 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_wb.png differ diff --git a/app/src/main/res/drawable-xxhdpi/infinite.png b/app/src/main/res/drawable-xxhdpi/infinite.png new file mode 100644 index 0000000..150116d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/infinite.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notif_no.png b/app/src/main/res/drawable-xxhdpi/notif_no.png new file mode 100644 index 0000000..ce6efeb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notif_no.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notif_yes.png b/app/src/main/res/drawable-xxhdpi/notif_yes.png new file mode 100644 index 0000000..d90886f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notif_yes.png differ diff --git a/app/src/main/res/drawable-xxhdpi/one.png b/app/src/main/res/drawable-xxhdpi/one.png new file mode 100644 index 0000000..b8ba58b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/one.png differ diff --git a/app/src/main/res/drawable-xxhdpi/one_red.png b/app/src/main/res/drawable-xxhdpi/one_red.png new file mode 100644 index 0000000..979138a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/one_red.png differ diff --git a/app/src/main/res/drawable-xxhdpi/question.png b/app/src/main/res/drawable-xxhdpi/question.png new file mode 100644 index 0000000..569768c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/question.png differ diff --git a/app/src/main/res/drawable-xxhdpi/star.png b/app/src/main/res/drawable-xxhdpi/star.png new file mode 100644 index 0000000..ecdb4b5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/star.png differ diff --git a/app/src/main/res/drawable-xxhdpi/three.png b/app/src/main/res/drawable-xxhdpi/three.png new file mode 100644 index 0000000..a632e2b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/three.png differ diff --git a/app/src/main/res/drawable-xxhdpi/three_red.png b/app/src/main/res/drawable-xxhdpi/three_red.png new file mode 100644 index 0000000..dbb3f86 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/three_red.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tuto1.png b/app/src/main/res/drawable-xxhdpi/tuto1.png new file mode 100644 index 0000000..d522ce2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tuto1.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tuto2.png b/app/src/main/res/drawable-xxhdpi/tuto2.png new file mode 100644 index 0000000..c4ed655 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tuto2.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tuto3.png b/app/src/main/res/drawable-xxhdpi/tuto3.png new file mode 100644 index 0000000..54f9794 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tuto3.png differ diff --git a/app/src/main/res/drawable-xxhdpi/two.png b/app/src/main/res/drawable-xxhdpi/two.png new file mode 100644 index 0000000..9652ea4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/two.png differ diff --git a/app/src/main/res/drawable-xxhdpi/two_red.png b/app/src/main/res/drawable-xxhdpi/two_red.png new file mode 100644 index 0000000..ec71a32 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/two_red.png differ diff --git a/app/src/main/res/drawable/button_with_borders.xml b/app/src/main/res/drawable/button_with_borders.xml new file mode 100644 index 0000000..cd3189a --- /dev/null +++ b/app/src/main/res/drawable/button_with_borders.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..34bff95 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/layout/activity_tuto.xml b/app/src/main/res/layout/activity_tuto.xml new file mode 100644 index 0000000..31a5fab --- /dev/null +++ b/app/src/main/res/layout/activity_tuto.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tuto1.xml b/app/src/main/res/layout/fragment_tuto1.xml new file mode 100644 index 0000000..7b10476 --- /dev/null +++ b/app/src/main/res/layout/fragment_tuto1.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_tuto2.xml b/app/src/main/res/layout/fragment_tuto2.xml new file mode 100644 index 0000000..d2d51e9 --- /dev/null +++ b/app/src/main/res/layout/fragment_tuto2.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_tuto3.xml b/app/src/main/res/layout/fragment_tuto3.xml new file mode 100644 index 0000000..c70e1af --- /dev/null +++ b/app/src/main/res/layout/fragment_tuto3.xml @@ -0,0 +1,24 @@ + + + + + +