# This file aims to do quick crash checking

import csv
import datetime
import os
import shutil
import subprocess
import time
from argparse import ArgumentParser, Namespace
from typing import List, Dict, Set

ALL_APPS = ['ActivityDiary', 'AmazeFileManager', 'and-bible', 'AnkiDroid', 'APhotoManager', 'commons',
            'collect', 'FirefoxLite', 'Frost', 'geohashdroid', 'MaterialFBook', 'nextcloud', 'Omni-Notes',
            'open-event-attendee-android', 'openlauncher', 'osmeditor4android', 'Phonograph', 'Scarlet-Notes',
            'sunflower', 'WordPress']

app_crash_data = {

    'AnkiDroid': {

        '#4200': ['com.ichi2.libanki.Note com.ichi2.libanki.Card.note()',
                  'com.ichi2.async.DeckTask.doInBackgroundUpdateNote(DeckTask.java',
                  'com.ichi2.async.DeckTask.doInBackground(DeckTask.java'],

        '#4451': [
            'android.support.design.widget.CoordinatorLayout$LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams',
            'com.ichi2.anki.AbstractFlashcardViewer.initLayout(AbstractFlashcardViewer.java',
            'com.ichi2.anki.AbstractFlashcardViewer.onCollectionLoaded(AbstractFlashcardViewer.java',
            'com.ichi2.anki.Reviewer.onCollectionLoaded(Reviewer.java',
            'com.ichi2.anki.AnkiActivity.onLoadFinished(AnkiActivity.java'],

        '#5638': ['com.ichi2.libanki.Utils.stripHTMLMedia',
                  'com.ichi2.libanki.Utils.stripHTML'],

        '#4707': ['exposed beyond app through ClipData.Item.getUri()',
                  'com.ichi2.anki.AnkiActivity.startActivityForResult(AnkiActivity.java',
                  'com.ichi2.anki.multimediacard.fields.BasicImageFieldController$2.onClick(BasicImageFieldController.java'],

        '#6145': ['com.ichi2.libanki.AnkiPackageExporter.exportInto(AnkiPackageExporter.java'],

        '#5756': ['Unable to start activity ComponentInfo{com.ichi2.anki/com.ichi2.anki.Reviewer}',
                  'com.ichi2.anki.AbstractFlashcardViewer.restoreCollectionPreferences(AbstractFlashcardViewer.java',
                  'com.ichi2.anki.AbstractFlashcardViewer.onCollectionLoaded(AbstractFlashcardViewer.java',
                  'com.ichi2.anki.AnkiActivity.startLoadingCollection(AnkiActivity.java'],

        '#4977': [
            "Attempt to invoke virtual method 'boolean android.support.v7.widget.SearchView.isIconified()' on a null object reference",
            'com.ichi2.anki.CardBrowser$20.onPostExecute(CardBrowser.java',
            'com.ichi2.async.DeckTask$TaskListener.onPostExecute(DeckTask.java',
            'com.ichi2.async.DeckTask.onPostExecute(DeckTask.java']
    },

    'ActivityDiary': {

        '#118': ['java.lang.IllegalArgumentException: position (',
                 'de.rampro.activitydiary.ui.generic.DetailRecyclerViewAdapter.getDiaryImageIdAt(DetailRecyclerViewAdapter.java',
                 'de.rampro.activitydiary.ui.history.HistoryRecyclerViewAdapter$1.onClick(HistoryRecyclerViewAdapter.java'],

        '#285': ['java.lang.NumberFormatException: For input string',
                 'de.rampro.activitydiary.ui.settings.SettingsActivity.updateLocationAge(SettingsActivity.java',
                 'de.rampro.activitydiary.ui.settings.SettingsActivity.onSharedPreferenceChanged(SettingsActivity.java']
    },

    'geohashdroid': {

        '#73': ['java.lang.RuntimeException: An error occurred while executing doInBackground()',
                'java.lang.String net.exclaimindustries.geohashdroid.util.Graticule.getLatitudeString(boolean)',
                'net.exclaimindustries.geohashdroid.util.HashBuilder$StockRunner.runStock(HashBuilder.java',
                'net.exclaimindustries.geohashdroid.services.StockService.onHandleWork(StockService.java']
    },

    'and-bible': {

        '#261': ['java.lang.StackOverflowError: stack size',
                 'org.crosswire.jsword.index.lucene.LuceneIndex.generateSearchIndexImpl(LuceneIndex.java'],

        '#375': ['kotlin.TypeCastException: null cannot be cast to non-null type org.crosswire.jsword.book.Book',
                 'net.bible.service.history.HistoryManager.setDumpString(HistoryManager.kt',
                 'net.bible.android.view.activity.page.MainBibleActivity.openTab(MainBibleActivity.kt'],

        '#480': ['net.bible.service.db.bookmark.BookmarkDBAdapter.updateLabel(BookmarkDBAdapter.kt',
                 'net.bible.android.control.bookmark.BookmarkControl.saveOrUpdateLabel(BookmarkControl.kt',
                 'net.bible.android.view.activity.bookmark.LabelDialogs$1.onClick(LabelDialogs.java'],

        '#697': [
            'Unable to start activity ComponentInfo{net.bible.android.activity/net.bible.android.view.activity.mynote.MyNotes}',
            'net.bible.android.control.versification.sort.VersificationPrioritiser.getVersifications(VersificationPrioritiser.java',
            'net.bible.android.control.versification.sort.ConvertibleVerseRangeComparator$Builder.withMyNotes(ConvertibleVerseRangeComparator.java'],

        '#703': ['org.crosswire.jsword.index.IndexStatus org.crosswire.jsword.book.Book.getIndexStatus()',
                 'net.bible.android.view.activity.search.SearchIndexProgressStatus.jobFinished(SearchIndexProgressStatus.java',
                 'net.bible.android.view.activity.base.ProgressActivityBase$initialiseView$uiUpdaterRunnable$1.run(ProgressActivityBase.kt']
    },

    'AmazeFileManager': {

        '#1232': ['Failure delivering result ResultInfo',
                  "Attempt to invoke virtual method 'java.lang.Object java.util.ArrayList.get(int)' on a null object reference",
                  'com.amaze.filemanager.activities.MainActivity.onActivityResult(MainActivity.java',
                  ],
        '#1558': ["com.amaze.filemanager.exceptions.StreamNotFoundException: Can't get stream",
                  "com.amaze.filemanager.filesystem.FileUtil.getOutputStream(FileUtil.java",
                  "com.amaze.filemanager.filesystem.FileUtil.isWritable(FileUtil.java",
                  'com.amaze.filemanager.asynchronous.asynctasks.WriteFileAbstraction.doInBackground'],

        '#1796': ["java.lang.IndexOutOfBoundsException: Index:",
                  'com.amaze.filemanager.asynchronous.asynctasks.DeleteTask.onPostExecute(DeleteTask.java'],

        '#1837': ['java.lang.IndexOutOfBoundsException: Index:',
                  'com.amaze.filemanager.adapters.glide.RecyclerPreloadModelProvider.getPreloadItems(RecyclerPreloadModelProvider.java',
                  'com.bumptech.glide.ListPreloader.preload(ListPreloader.java',
                  'com.bumptech.glide.integration.recyclerview.RecyclerToListViewScrollListener.onScrolled(RecyclerToListViewScrollListener.java']
    },

    'FirefoxLite': {

        '#4881': ['org.json.JSONException: End of input at character',
                  'org.mozilla.rocket.util.JsonUtilsKt.toJsonArray(JsonUtils.kt',
                  'org.mozilla.rocket.home.contenthub.data.ContentHubRepoKt.jsonStringToTypeList(ContentHubRepo.kt',
                  'org.mozilla.rocket.home.contenthub.data.ContentHubRepo$getReadTypesLive$1.invoke(ContentHubRepo.kt',
                  'org.mozilla.rocket.extension.LiveDataExtensionKt$sam$androidx_arch_core_util_Function$0.apply(LiveDataExtension.kt)',
                  'org.mozilla.focus.activity.MainActivity.onStart(MainActivity.kt',
                  ],

        '#4942': [
            'java.lang.RuntimeException: Cannot create an instance of class org.mozilla.rocket.home.HomeViewModel',
            'java.lang.InstantiationException: java.lang.Class<org.mozilla.rocket.home.HomeViewModel> has no zero argument constructor',
            'org.mozilla.focus.tabs.tabtray.TabTrayFragment.onCreateView(TabTrayFragment.java'],

        '#5085': [
            "java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.os.BaseBundle.getString(java.lang.String)' on a null object reference",
            'org.mozilla.focus.settings.SettingsFragment.onCreate(SettingsFragment.kt']

    },

    'open-event-attendee-android': {
        '#2198': [
            'org.fossasia.openevent.general.search.SearchFragment$onCreateView$3.onClick(SearchFragment.kt',
            # special handling for combo, which discarded the line number on this case
            # 'org.fossasia.openevent.general.search.SearchFilterFragment$setFilters$3.onClick(SearchFilterFragment.kt:127)',
            'org.fossasia.openevent.general.search.type.SearchTypeFragment.<init>(SearchTypeFragment.kt']
    },

    'openlauncher': {
        '#67': ['java.lang.SecurityException: Not allowed to change Do Not Disturb state',
                'com.benny.openlauncher.util.LauncherAction.RunAction(LauncherAction.java',
                'com.benny.openlauncher.activity.Home$11.onItemClick(Home.java']
    },

    'APhotoManager': {
        '#116': ['Error inflating class de.k3b.android.widgets.EditTextPreferenceWithSummary',
                 'de.k3b.android.androFotoFinder.SettingsActivity.onCreate(SettingsActivity.java']
    },

    'sunflower': {
        '#239': [
            'java.lang.IllegalArgumentException: navigation destination com.google.samples.apps.sunflower:id/action_plant_list_fragment_to_plant_detail_fragment is unknown to this NavController',
            'com.google.samples.apps.sunflower.adapters.PlantAdapter$createOnClickListener$1.onClick(PlantAdapter.kt']
    },

    'collect': {
        '#3222': ["java.lang.IllegalArgumentException: column 'MAX(date)' does not exist",

                  'org.odk.collect.android.activities.FormChooserList.onLoadFinished(FormChooserList.java']
    },

    'MaterialFBook': {
        '#224': ['android.content.res.Resources$NotFoundException: Resource ID #0x20b001b',
                 'org.chromium.ui.base.DeviceFormFactor.b(PG',
                 'org.chromium.ui.base.DeviceFormFactor.a(PG',
                 'bCE.onCreateActionMode(PG',
                 'org.chromium.content.browser.selection.SelectionPopupControllerImpl.y(PG',
                 'org.chromium.content.browser.selection.SelectionPopupControllerImpl.showSelectionMenu(PG']
    },

    'Omni-Notes': {
        '#745': ['No virtual method fitCenter()Lcom/bumptech/glide/request/RequestOptions',
                 'it.feio.android.simplegallery.GalleryPagerFragment.onCreateView(GalleryPagerFragment.java']
    },

    'OmniNotes': {
        '#745': ['No virtual method fitCenter()Lcom/bumptech/glide/request/RequestOptions',
                 'it.feio.android.simplegallery.GalleryPagerFragment.onCreateView(GalleryPagerFragment.java']
    },

    'Phonograph': {
        '#112': ['com.kabouzeid.gramophone.appshortcuts.AppShortcutLauncherActivity.startServiceWithSongs']
    },

    'osmeditor': {
        '#637': [
            "Attempt to invoke interface method 'java.util.Set java.util.Map.entrySet()' on a null object reference"],

        '#705': [
            "android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views",
            "de.blau.android.propertyeditor.PresetFragment$3.doInBackground(PresetFragment.java:258)",
            "de.blau.android.propertyeditor.PresetFragment$3.doInBackground(PresetFragment.java:244)"],

        '#729': [
            "android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views",
            "de.blau.android.propertyeditor.PresetFragment$3.doInBackground(PresetFragment.java:258)",
            "de.blau.android.propertyeditor.PresetFragment$3.doInBackground(PresetFragment.java:244)"]
    },

    'Scarlet-Notes': {
        '#114': ['java.lang.Exception: Invalid Search Mode',
                 'com.maubis.scarlet.base.support.SearchConfigKt.getNotesForMode(SearchConfig.kt',
                 'com.maubis.scarlet.base.support.SearchConfigKt.filterSearchWithoutFolder(SearchConfig.kt',
                 'com.maubis.scarlet.base.support.SearchConfigKt.unifiedSearchSynchronous(SearchConfig.kt',
                 'com.maubis.scarlet.base.MainActivity$unifiedSearchSynchronous$$inlined$map$lambda$1.invokeSuspend(MainActivity.kt']

    },

    'Frost': {

        '#1323': [
            "java.net.UnknownHostException: Unable to resolve host \"m.facebook.com\": No address associated with hostname",
            "com.pitchedapps.frost.fragments.FrostParserFragment.reloadImpl$suspendImpl(RecyclerFragmentBase.kt",
            "com.pitchedapps.frost.fragments.FrostParserFragment.reloadImpl(Unknown Source",
            "com.pitchedapps.frost.fragments.RecyclerFragment$reload$2.invokeSuspend(RecyclerFragmentBase.kt"]
    },

    'commons': {
        '#1385': ['java.lang.NullPointerException: Callable returned null'],
        '#1391': ["fr.free.nrw.commons.nearby.NearbyMapFragment$8.onStateChanged(NearbyMapFragment.java"],

        '#1581': [
            "java.lang.NullPointerException: Attempt to invoke virtual method 'double android.location.Location.getLatitude()' on a null object reference",
            "fr.free.nrw.commons.location.LatLng.from(LatLng.java",
            'fr.free.nrw.commons.location.LocationServiceManager.getLKL(LocationServiceManager.java',
            'fr.free.nrw.commons.nearby.NearbyActivity.onRequestPermissionsResult(NearbyActivity.java'],

        '#2123': [
            "java.lang.NullPointerException: Attempt to invoke virtual method 'android.support.v4.app.FragmentActivity android.support.v4.app.Fragment.getActivity()' on a null object reference",
            'fr.free.nrw.commons.media.MediaDetailPagerFragment$MediaDetailAdapter.getItem(MediaDetailPagerFragment.java'],
        '#3244': ['fr.free.nrw.commons.upload.UploadActivity.receiveInternalSharedItems(UploadActivity.java']
    },

    'nextcloud': {
        '#1918': [
            'java.lang.ClassCastException: com.owncloud.android.ui.preview.PreviewImageActivity cannot be cast to com.owncloud.android.ui.activity.FileDisplayActivity',
            'com.owncloud.android.ui.preview.PreviewImageFragment.onOptionsItemSelected(PreviewImageFragment.java'],

        '#4026': [
            'java.lang.RuntimeException: Unable to start activity ComponentInfo{com.nextcloud.client/com.owncloud.android.ui.activity.FileDisplayActivity}',
            'com.owncloud.android.ui.activity.ToolbarActivity.setupToolbar(ToolbarActivity.java',
            'com.owncloud.android.ui.activity.FileDisplayActivity.onCreate(FileDisplayActivity.java'],

        '#4792': [
            "java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String com.owncloud.android.datamodel.OCFile.getRemotePath()' on a null object reference",
            'com.owncloud.android.ui.dialog.CreateFolderDialogFragment.onClick(CreateFolderDialogFragment.java'],

        '#5173': ['android.view.MenuItem android.view.MenuItem.setVisible(boolean)',
                  'com.owncloud.android.ui.fragment.OCFileListFragment.onPrepareOptionsMenu(OCFileListFragment.java']
    },

    'WordPress': {
        '#6530': ['org.wordpress.android.fluxc.store.PostStore.onAction(PostStore.java'],
        '#7182': [
            'org.wordpress.android.login.LoginUsernamePasswordFragment.onSiteChanged(LoginUsernamePasswordFragment.java'],
        '#8659': [
            'Two different ViewHolders have the same stable ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT change.'],
        '#10302': [
            'org.wordpress.android.login.LoginBaseFormFragment.onOptionsItemSelected(LoginBaseFormFragment.java'],

        '#10363': ['java.lang.IllegalStateException: itemView.findViewById(R.id.container) must not be null',
                   'org.wordpress.android.ui.posts.PostListItemViewHolder.<init>(PostListItemViewHolder.kt',
                   'org.wordpress.android.ui.posts.PostListItemViewHolder$Compact.<init>(PostListItemViewHolder.kt',
                   'org.wordpress.android.ui.posts.adapters.PostListAdapter.onCreateViewHolder(PostListAdapter.kt'
                   ],

        '#10547': [
            'start activity ComponentInfo{org.wordpress.android/org.wordpress.android.ui.posts.EditPostActivity}: java.lang.IllegalArgumentException: PostLoadingState wrong value 6',
            'org.wordpress.android.ui.posts.EditPostActivity$PostLoadingState.fromInt(EditPostActivity.java',
            'org.wordpress.android.ui.posts.EditPostActivity.onCreate(EditPostActivity.java'
        ],

        '#10876': ['in WithSelect(WithDispatch(WithViewportMatch(WithPreferredColorScheme(Component))))'],

        '#11135': [
            'java.lang.IllegalStateException: siteStore.getSiteBySiteI…t.getLong(EXTRA_SITE_ID)) must not be null',
            'org.wordpress.android.ui.CommentFullScreenDialogFragment.onCreateView(CommentFullScreenDialogFragment.kt'],

        '#11992': [
            "java.lang.NullPointerException: Attempt to invoke virtual method 'boolean org.wordpress.android.ui.FilteredRecyclerView.isRefreshing()' on a null object reference",
            'org.wordpress.android.ui.reader.ReaderPostListFragment.onSaveInstanceState(ReaderPostListFragment.java']
    }
}


def get_testing_result_dir(all_testing_result_dirs, app_name, issue_id):
    tmp_paths = []
    for result_dir_path in all_testing_result_dirs:
        result_dir_basename = os.path.basename(result_dir_path)
        if app_name in result_dir_basename and issue_id in result_dir_basename:
            tmp_paths.append(result_dir_path)
    return tmp_paths


def get_app_name(testing_result_dir):
    for app_name in ALL_APPS:
        if os.path.basename(testing_result_dir).startswith(app_name):
            return app_name
    print("Warning: cannot find app name for this testing result dir: %s" % testing_result_dir)


def get_apk_info(testing_result_dir: str, app_name: str):
    base_name = os.path.basename(testing_result_dir)
    target_apk_file_name = str(base_name.split(".apk")[0]) + ".apk"

    target_apk_file_path = os.path.join("../" + app_name, target_apk_file_name)
    get_app_package_name_cmd = "aapt dump badging " + target_apk_file_path + " | grep package | awk '{print $2}' | sed s/name=//g | sed s/\\'//g"
    app_package_name = ""

    try:
        p = subprocess.Popen(get_app_package_name_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        # clear the output
        app_package_name = p.communicate()[0].decode('utf-8').strip()
        print(app_package_name)
    except os.error as e:
        print(e)

    return target_apk_file_name, app_package_name


def main(args: Namespace):
    # collect all testing result dirs
    all_testing_results_dirs = []
    subdirs = os.listdir(args.o)
    for subdir in subdirs:
        # print(subdir)
        subdir_path = os.path.join(args.o, subdir)
        if os.path.isdir(subdir_path):
            all_testing_results_dirs.append(subdir_path)

    # print(all_testing_results_dirs)
    # the dict only used for collecting non-target crashes
    #   key: the apk file name
    #   value: list of signatures of crash stacks
    other_crashes_signature_str_dict: Dict[str, List[str]] = {}
    other_crashes_complete_exception_trace_dict: Dict[str, List[List[str]]] = {}

    # check crash
    for app_name in app_crash_data:

        if args.app_name is not None and args.app_name != app_name:
            # skip unrelated apps if args.app_name is given
            continue

        # if args.monkey and app_name == "ActivityDiary":
        #    # TODO special check for Monkey's ActivityDiary [should be removed in the future]
        #    continue

        print(args.app_name)

        issue_crash_data = app_crash_data[app_name]

        for issue_id in issue_crash_data:

            if args.issue_id is not None and args.issue_id != issue_id:
                # skip unrelated issues if args.issue_id is given
                continue

            issue_testing_result_dirs = get_testing_result_dir(all_testing_results_dirs,
                                                               app_name,
                                                               issue_id)

            if len(issue_testing_result_dirs) == 0:
                continue

            log_tag_name = '[' + app_name + ', ' + issue_id + '] '

            print("\n\n=========")

            crash_signature_strs = issue_crash_data[issue_id]

            # scanning the testing results of the given issue
            for result_dir in issue_testing_result_dirs:

                logcat_file_path = ""
                testing_time_file_path = ""
                login_file_path = ""

                if args.monkey:
                    # Monkey
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "monkey_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.ape:
                    # Ape
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "ape_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.combo:
                    # ComboDroid
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "combo_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H-%M-%S'

                if args.timemachine:
                    # TimeMachine
                    logcat_file_path = os.path.join(result_dir, "timemachine-output/crashes.log")
                    login_file_path = os.path.join(result_dir, "timemachine-run.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "timemachine-output/run_time.log")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.humanoid:
                    # Humanoid
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "humanoid_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.sapienz:
                    # Sapienz
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "sapienz_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.qtesting:
                    # Q-testing
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "qtesting_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.weighted:
                    # Q-testing
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "weighted_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if args.fastbot:
                    # Fastbot
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "fastbot_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'
                    
                if args.wetest:
                    # WeTest
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "wetest_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'
                
                if args.newmonkey:
                    # Newmonkey
                    logcat_file_path = os.path.join(result_dir, "logcat.log")
                    login_file_path = os.path.join(result_dir, "login.log")
                    testing_time_file_path = os.path.join(result_dir,
                                                          "newmonkey_testing_time_on_emulator.txt")
                    testing_time_datetime_str = '%Y-%m-%d-%H:%M:%S'

                if os.path.exists(logcat_file_path) and os.path.exists(testing_time_file_path):

                    print('\n')
                    print(log_tag_name + 'scanning (%s) ' % os.path.basename(result_dir))

                    testing_time_file = open(testing_time_file_path, 'r')
                    lines = testing_time_file.readlines()
                    for line in lines:
                        print(log_tag_name + 'testing time: %s ' % line.strip())
                    if len(lines) == 0:
                        # double check on the file content on the testing time
                        print(log_tag_name + 'this run does not have recorded testing time, skip this run!')
                        continue

                    time.sleep(1)

                    # get the start testing datetime
                    if args.timemachine:
                        # special handle timemachine
                        start_testing_datetime_str = lines[0][0:19]
                    else:
                        start_testing_datetime_str = lines[0].strip()
                    print("the start testing time is: %s" % start_testing_datetime_str)
                    start_testing_datetime_obj = datetime.datetime.strptime(start_testing_datetime_str,
                                                                            testing_time_datetime_str)
                    print("the start testing time (parsed) is: %s" % start_testing_datetime_obj)

                    if os.path.exists(login_file_path):
                        cmd = 'grep ' + "\"" + "Login SUCCESS" + "\" " + login_file_path
                        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
                        # clear the output
                        output = p.communicate()[0].decode('utf-8').strip()
                        if len(output) != 0:
                            print(log_tag_name + "Login SUCCESS")
                        else:
                            print(log_tag_name + "Login FAIL?")

                    # split the logcat file into separate stack traces
                    crash_stack_traces: Dict[str, List[str]] = {}
                    logcat_file = open(logcat_file_path, 'r')

                    if args.timemachine:

                        if not args.other_crashes:
                            # special handle for TimeMachine (for target crash)
                            cache_list = []
                            for line in logcat_file.readlines():
                                if line.startswith("---"):
                                    continue

                                if line.startswith("["):
                                    time_label = line.strip()
                                    crash_stack_traces[time_label] = cache_list
                                    cache_list = []
                                else:
                                    cache_list.append(line)
                        else:
                            # special handle for TimeMachine (for non-target crash)
                            fake_time_label_index = 1
                            fake_time_label = "fake_time_"
                            for line in logcat_file.readlines():
                                if line.startswith("---") or line.startswith("["):
                                    fake_time_label_index += 1
                                    fake_time_label = fake_time_label + str(fake_time_label_index)
                                    continue

                                if fake_time_label not in crash_stack_traces:
                                    crash_stack_traces[fake_time_label] = [line]
                                else:
                                    crash_stack_traces[fake_time_label].append(line)

                    else:
                        for line in logcat_file.readlines():
                            if line.startswith("---"):
                                continue

                            res = [i for i in range(len(line)) if line.startswith(':', i)]
                            try:
                                # hot fix
                                time_label = line[0:res[2]]  # the third ":" is the split point
                            except IndexError:
                                print("Catch IndexError when paring logcat!")
                                continue
                            if time_label not in crash_stack_traces:
                                crash_stack_traces[time_label] = [line]
                            else:
                                crash_stack_traces[time_label].append(line)
                    logcat_file.close()

                    if args.other_crashes:

                        # check the number of other crashes that were detected by-product
                        app_dir_name = get_app_name(result_dir)
                        apk_file_name, app_package_name = get_apk_info(result_dir, app_dir_name)

                        print("apk file name: %s" % apk_file_name)
                        print("apk package name: %s" % app_package_name)

                        if app_package_name.endswith(".debug") or app_package_name.endswith(".debug.ting"):
                            app_package_name = app_package_name.split(".debug")[0]
                        if app_package_name.endswith(".client"):
                            app_package_name = "com.owncloud.android"
                        if app_package_name.endswith(".activity"):
                            app_package_name = app_package_name.split(".activity")[0]
                        if app_package_name.endswith(".quicknote"):
                            app_package_name = "com.maubis.scarlet"
                        if app_package_name.endswith(".attendee"):
                            app_package_name = "org.fossasia.openevent"

                        for time_label in crash_stack_traces:

                            if args.timemachine:
                                # Special handling on timemachine

                                target_stack = crash_stack_traces[time_label]

                                # split the target_stack into sub exception traces
                                sub_exception_stacks: Dict[str, List[str]] = {}
                                for line in target_stack:
                                    res = [i for i in range(len(line)) if line.startswith(':', i)]
                                    print(line)
                                    if len(res) < 3:
                                        # special handling for the case like
                                        #   "AndroidRuntime: 	at android.os.BinderProxy.transactNative(Native Method)"
                                        continue
                                    else:
                                        local_time_label = line[0:res[2]]  # the third ":" is the split point
                                    if local_time_label not in sub_exception_stacks:
                                        sub_exception_stacks[local_time_label] = [line]
                                    else:
                                        sub_exception_stacks[local_time_label].append(line)

                                for local_time_label in sub_exception_stacks:
                                    if "AndroidRuntime" in local_time_label or "ACRA" in local_time_label \
                                            or "CustomActivityOnCrash" in local_time_label \
                                            or "CrashAnrDetector" in local_time_label:
                                        # focus on "AndroidRuntime" bugs
                                        pass
                                    else:
                                        continue

                                    uniqe_signature_of_crash_stack = ""

                                    sub_exception_stack = sub_exception_stacks[local_time_label]
                                    for line in sub_exception_stack:
                                        if "at " in line and app_package_name in line:
                                            line_without_time_label = line.replace(local_time_label + ":", "").strip()
                                            uniqe_signature_of_crash_stack += line_without_time_label

                                    if uniqe_signature_of_crash_stack == "":
                                        continue

                                    print("\n-- signature --")
                                    print(uniqe_signature_of_crash_stack)
                                    print("----")

                                    if apk_file_name not in other_crashes_signature_str_dict:
                                        other_crashes_signature_str_dict[apk_file_name] = [
                                            uniqe_signature_of_crash_stack]
                                        other_crashes_complete_exception_trace_dict[apk_file_name] = [
                                            sub_exception_stack]
                                    else:
                                        if uniqe_signature_of_crash_stack not in other_crashes_signature_str_dict[
                                            apk_file_name]:
                                            # check existence
                                            other_crashes_signature_str_dict[apk_file_name].append(
                                                uniqe_signature_of_crash_stack)
                                            other_crashes_complete_exception_trace_dict[apk_file_name].append(
                                                sub_exception_stack)

                            else:

                                target_stack = crash_stack_traces[time_label]
                                uniqe_signature_of_crash_stack = ""

                                if "AndroidRuntime" in time_label or "ACRA" in time_label \
                                        or "CustomActivityOnCrash" in time_label \
                                        or "CrashAnrDetector" in time_label:
                                    # focus on "AndroidRuntime" bugs
                                    pass
                                else:
                                    continue

                                for line in target_stack:

                                    if "at " in line and app_package_name in line:
                                        line_without_time_label = line.replace(time_label + ":", "").strip()
                                        uniqe_signature_of_crash_stack += line_without_time_label

                                if uniqe_signature_of_crash_stack == "":
                                    continue

                                print("\n-- signature --")
                                print(uniqe_signature_of_crash_stack)
                                print("----")

                                if apk_file_name not in other_crashes_signature_str_dict:
                                    other_crashes_signature_str_dict[apk_file_name] = [uniqe_signature_of_crash_stack]
                                    other_crashes_complete_exception_trace_dict[apk_file_name] = [target_stack]
                                else:
                                    if uniqe_signature_of_crash_stack not in other_crashes_signature_str_dict[
                                        apk_file_name]:
                                        # check existence
                                        other_crashes_signature_str_dict[apk_file_name].append(
                                            uniqe_signature_of_crash_stack)
                                        other_crashes_complete_exception_trace_dict[apk_file_name].append(target_stack)

                            # print("--")
                            # print(len(set(other_crashes_dict[app_name])))
                            # print(other_crashes_dict[app_name])
                            # print("--")

                        if apk_file_name in other_crashes_signature_str_dict:
                            number_of_unique_other_crashes = len(set(other_crashes_signature_str_dict[apk_file_name]))
                            print("#unique other crashes: %d" % number_of_unique_other_crashes)
                        else:
                            number_of_unique_other_crashes = 0

                        # output to final result file
                        if args.final_result_csv_file_path is not None:
                            with open(args.final_result_csv_file_path, "a") as csv_file:
                                writer = csv.writer(csv_file)
                                writer.writerow([apk_file_name, issue_id, os.path.basename(result_dir),
                                                 number_of_unique_other_crashes])
                            csv_file.close()

                    else:
                        # check the existence of target crashes (i.e., the critical crash we concern)
                        number_of_matched_crash = 0
                        crash_triggering_time_durations = []
                        for time_label in crash_stack_traces:
                            target_stack = crash_stack_traces[time_label]

                            is_matched = True
                            for signature_str in crash_signature_strs:

                                exist = False
                                for line in target_stack:
                                    if signature_str in line:
                                        exist = True
                                        break
                                if exist:
                                    continue
                                else:
                                    is_matched = False
                                    break

                            if is_matched:

                                # compute the time duration to trigger the crash
                                if args.timemachine:
                                    # print("time label: %s" % time_label)
                                    matched_datetime_str = time_label.replace("[", "").replace("]", "")
                                    crash_triggering_datetime_obj = datetime.datetime.strptime(matched_datetime_str,
                                                                                               '%Y-%m-%d-%H:%M:%S')
                                else:
                                    matched_datetime_str = "2020-" + time_label.split('.')[0]
                                    crash_triggering_datetime_obj = datetime.datetime.strptime(matched_datetime_str,
                                                                                               '%Y-%m-%d %H:%M:%S')

                                tmp_time_duration_in_minutes = (
                                                                       crash_triggering_datetime_obj - start_testing_datetime_obj).total_seconds() / 60
                                crash_triggering_time_durations.append("{:.0f}".format(tmp_time_duration_in_minutes))

                                number_of_matched_crash += 1

                        if number_of_matched_crash > 0:
                            print(log_tag_name + "the crash was triggered (%d) times" % number_of_matched_crash)
                            print(log_tag_name + "the time duration: %s (mins)" % crash_triggering_time_durations)
                            # if args.v:
                            #     print('---')
                            #     # verbose mode
                            #     print(output)
                            #     print('---')

                        # output to final result file
                        if args.final_result_csv_file_path is not None:

                            if not args.simple_format:
                                with open(args.final_result_csv_file_path, "a") as csv_file:
                                    writer = csv.writer(csv_file)
                                    writer.writerow([app_name, issue_id, os.path.basename(result_dir),
                                                     len(crash_triggering_time_durations)])
                                    for time_duration in crash_triggering_time_durations:
                                        writer.writerow(["", "", "", time_duration])
                                csv_file.close()
                            else:

                                if len(crash_triggering_time_durations) > 0:
                                    # output in a simple format: only output triggered bugs and its first triggering time
                                    with open(args.final_result_csv_file_path, "a") as csv_file:
                                        writer = csv.writer(csv_file)
                                        writer.writerow([app_name, issue_id, os.path.basename(result_dir),
                                                         len(crash_triggering_time_durations),
                                                         crash_triggering_time_durations[0]])
                                    csv_file.close()

                else:
                    print('\n')
                    print(log_tag_name + 'scanning (%s) ' % os.path.basename(result_dir))
                    print(log_tag_name + "Warning: logcat file or testing time file in (%s) does not exist!" %
                          os.path.basename(result_dir))
                    continue

    if args.other_crashes:

        total_number_of_other_crashes = 0
        for apk_file_name in other_crashes_signature_str_dict:
            print("%s: %d" % (apk_file_name, len(set(other_crashes_signature_str_dict[apk_file_name]))))
            total_number_of_other_crashes += len(set(other_crashes_signature_str_dict[apk_file_name]))

        print("#total other crashes: %d" % total_number_of_other_crashes)

        # output the complete stack traces of all the other crashes (i.e., non-target crashes)
        tmp_output_dir = os.path.join(args.o, "other_crashes")
        if os.path.exists(tmp_output_dir):
            shutil.rmtree(tmp_output_dir)

        for apk_file_name in other_crashes_complete_exception_trace_dict:
            print("dump complete crash stack for: %s" % apk_file_name)
            tmp_output_dir_of_each_apk = os.path.join(tmp_output_dir, apk_file_name)
            if not os.path.exists(tmp_output_dir_of_each_apk):
                os.makedirs(tmp_output_dir_of_each_apk)
            stack_traces: List[List[str]] = other_crashes_complete_exception_trace_dict[apk_file_name]
            file_index = 0
            for stack_trace in stack_traces:
                tmp_file_path = os.path.join(tmp_output_dir_of_each_apk, "crash_stack_" + str(file_index) + ".txt")
                file_index += 1
                with open(tmp_file_path, "w") as tmp_file:
                    for line in stack_trace:
                        tmp_file.write(line)
                tmp_file.close()


if __name__ == '__main__':

    ap = ArgumentParser()

    ap.add_argument('-o', required=True, help="the output directory of testing results")
    ap.add_argument('-v', default=False, action='store_true')

    # supported fuzzing tools
    ap.add_argument('--monkey', default=False, action='store_true')
    ap.add_argument('--ape', default=False, action='store_true')
    ap.add_argument('--timemachine', default=False, action='store_true')
    ap.add_argument('--combo', default=False, action='store_true')
    ap.add_argument('--humanoid', default=False, action='store_true')
    ap.add_argument('--sapienz', default=False, action='store_true')
    ap.add_argument('--qtesting', default=False, action='store_true')
    ap.add_argument('--weighted', default=False, action='store_true')
    ap.add_argument('--fastbot', default=False, action='store_true')
    ap.add_argument('--wetest', default=False, action='store_true')
    ap.add_argument('--newmonkey', default=False, action='store_true')

    ap.add_argument('--app', type=str, dest='app_name')
    ap.add_argument('--id', type=str, dest='issue_id')
    ap.add_argument('--csv', type=str, dest='final_result_csv_file_path')
    ap.add_argument('--simple', default=False, action='store_true', dest='simple_format',
                    help="standard output in a simple format")
    ap.add_argument('--other_crashes', default=False, action='store_true', dest='other_crashes')

    args = ap.parse_args()

    if not os.path.exists(args.o):
        ap.error("Error: the output directory does not exist!")

    if args.final_result_csv_file_path is not None and os.path.exists(args.final_result_csv_file_path):
        os.remove(args.final_result_csv_file_path)

    main(args)
