2

After migrating this next Java class:

public class ImportExportActivity extends BaseActivity
{
    private String filename;
    private static Intent oData;
    private ActivityResultLauncher<Intent> activityResultLauncher;
    private ActivityImportexportBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setImportDataActivityResultHandler();
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void setImportDataActivityResultHandler(){
        activityResultLauncher =
                registerForActivityResult(
                        new ActivityResultContracts.StartActivityForResult(),
                        result -> {
                            if (result.getResultCode() == Activity.RESULT_OK) {
                                Intent data = result.getData();
                                if (data != null) {
                                    oData = data;
                                    boolean hasPermission =
                                            TMPermission.verifyStorageReadPermissions((Activity)AppSettings.getContext());
                                    appSettings.setRequestPermissionSource(RequestPermissionSource.IMPORTDATA);
                                    if (hasPermission) {
                                        String strResult = TMImport.importData(data);
                                        TMToast toast = new TMToast(AppSettings.getContext());
                                        toast.show(strResult, Toast.LENGTH_LONG);
                                    }
                                }
                            }
                        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(appSettings.getRequestPermissionSource()
                .equals(RequestPermissionSource.IMPORTDATA)){
            if (TMPermission.hasReadPermission()) {
                String strResult = TMImport.importData(oData);
                TMToast toast = new TMToast(this);
                toast.show(strResult, Toast.LENGTH_LONG);
            }
        }
    }

    public void setActivityLayout()
    {
        super.setActivityLayout();
        setButtonsAppearance();
    }

    public void setContentView()
    {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_importexport);
    }

    public void setActivityBackground(){
        binding.llImportExportMain.setBackgroundColor(Color.parseColor(
                appSettings.mainLayoutBackgroundColor
        ));
    }

    public void createActionBar()
    {
        String activityTitle = TMLocale.getStringResourceByName("activityrecords_textview_importexport").toUpperCase();
        TMAppearance.createActionBar(this, activityTitle);
    }

    public void setFooterMenu()
    {
        FootermenuBinding footerBinding = binding.footermenu;
        TMFooter.footerBinding = new WeakReference<>(footerBinding);
        TMFooter.activity = new WeakReference<>(this);
        TMFooter.dialog = new WeakReference<>(dialog);
        TMFooter.setFooterMenu();
    }

    private void setButtonsAppearance()
    {
        configureButton(binding.btnImport);
        configureButton(binding.btnExport);

        setButtonClickListener(binding.cvImport, Enum.Action.IMPORT);
        setButtonClickListener(binding.cvExport, Enum.Action.EXPORT);
    }

    @SuppressLint("ClickableViewAccessibility")
    private void setButtonClickListener(CardView button, String target)
    {
        button.setOnTouchListener((v, event) -> {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                TMSound.playButtonSound(this);
                setButtonStyle_Pressed(button);
            } else {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    setButtonStyle_Released(button);
                    switch (target) {
                        case (Enum.Action.IMPORT):
                            selectFile();
                            break;
                        case (Enum.Action.EXPORT):
                            onTouchExport();
                            break;
                    }
                }
            }
            return true;
        });
    }

    private void onTouchExport()
    {
        //check if there is data to export
        TMSession us = new TMSession();
        User user = us.getUserSession();
        List<RecordDTO> records = UserService.getRecords(user.getId(), "");

        if (records.size()>0){
            exportData();
        }else{
            String strError = TMLocale.getStringResourceByName("activityimportexport_nodatatoexport");
            TMToast toast = new TMToast(this);
            toast.show(strError, Toast.LENGTH_SHORT);
        }
    }

    private void configureButton(TextView button)
    {
        int dpButtonHeight = TMActivity.getButtonsHeight(3); // TMScreen.dp2px(height);
        button.setHeight(dpButtonHeight);
        button.setTextSize(TMAppearance.getPrefsByScreenDensity()[3]);
    }
    
    /**
     * Imports records data from file into records table
     */
    private void selectFile()
    {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        activityResultLauncher.launch(intent);
    }

    private void exportData()
    {
        String recordsData = TMExport.getRecordsData();
        try {
            filename = getFilename();
            File filelocation = TMFile.writeFile(this, filename, recordsData);
            sendExportFileTo(filelocation);
        } catch (IOException e) {
            TMToast toast = new TMToast(this);
            toast.show(e.getMessage(), Toast.LENGTH_SHORT);
            ExceptionHandler.logException(e);
        }
    }

    private String getFilename()
    {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.ENGLISH);
        Date now = new Date();
        return "testme_exportfile_" + formatter.format(now) + ".txt";
    }

    private void sendExportFileTo(File attachment)
    {
        Uri uri = FileProvider.getUriForFile(this, this.getApplicationContext().getPackageName() + ".provider", attachment);
        Intent i = new Intent(Intent.ACTION_SEND);
        i.setType("text/plain");
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        i.putExtra(Intent.EXTRA_EMAIL, new String[] { "" });

        String subject = TMLocale.getStringResourceByName("importexport_exporttomail_subject") + " " + filename;
        String body = TMLocale.getStringResourceByName("importexport_exporttomail_body");

        i.putExtra(Intent.EXTRA_SUBJECT, subject);
        i.putExtra(Intent.EXTRA_TEXT, body);
        i.putExtra(Intent.EXTRA_STREAM, uri);

        try {
            this.startActivity(Intent.createChooser(i, "Send mail..."));
        } catch (android.content.ActivityNotFoundException e) {
            Toast.makeText(this,
                    TMLocale.getStringResourceByName("importexport_exporttomail_noemailclients"), Toast.LENGTH_SHORT)
                    .show();
            ExceptionHandler.logException(e);
        }
    }
}

to Kotlin I ended with the. next class:

class ImportExportActivity : BaseActivity() {
    private var filename: String? = null
    private var activityResultLauncher: ActivityResultLauncher<Intent>? = null
    private var binding: ActivityImportexportBinding? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setImportDataActivityResultHandler()
    }

    private fun setImportDataActivityResultHandler() {
        activityResultLauncher = registerForActivityResult<Intent, ActivityResult>(
            StartActivityForResult(),
            ActivityResultCallback { result: ActivityResult ->
                if (result.resultCode == Activity.RESULT_OK) {
                    val data = result.data
                    if (data != null) {
                        oData = data
                        val hasPermission =
                            TMPermission.verifyStorageReadPermissions(AppSettings.getContext() as Activity)
                        appSettings.requestPermissionSource = RequestPermissionSource.IMPORTDATA
                        if (hasPermission) {
                            val strResult = TMImport.importData(data)
                            val toast = TMToast(AppSettings.getContext())
                            toast.show(strResult, Toast.LENGTH_LONG)
                        }
                    }
                }
            })
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (appSettings.requestPermissionSource
            == RequestPermissionSource.IMPORTDATA
        ) {
            if (TMPermission.hasReadPermission()) {
                val strResult = TMImport.importData(oData)
                val toast = TMToast(this)
                toast.show(strResult, Toast.LENGTH_LONG)
            }
        }
    }

    override fun setActivityLayout() {
        super.setActivityLayout()
        setButtonsAppearance()
    }

    override fun setContentView() {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_importexport)
    }

    override fun setActivityBackground() {
        binding!!.llImportExportMain.setBackgroundColor(
            Color.parseColor(
                appSettings.mainLayoutBackgroundColor
            )
        )
    }

    override fun createActionBar() {
        val activityTitle =
            TMLocale.getStringResourceByName("activityrecords_textview_importexport").uppercase(
                Locale.getDefault()
            )
        createActionBar(this, activityTitle)
    }

    override fun setFooterMenu() {
        val footerBinding = binding!!.footermenu
        TMFooter.footerBinding = WeakReference(footerBinding)
        TMFooter.activity = WeakReference(this)
        TMFooter.dialog = WeakReference(dialog)
        TMFooter.setFooterMenu()
    }

    private fun setButtonsAppearance() {
        configureButton(binding!!.btnImport)
        configureButton(binding!!.btnExport)
        setButtonClickListener(binding!!.cvImport, Enum.Action.IMPORT)
        setButtonClickListener(binding!!.cvExport, Enum.Action.EXPORT)
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun setButtonClickListener(button: CardView, target: String) {
        button.setOnTouchListener { v: View?, event: MotionEvent ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                TMSound.playButtonSound(this)
                setButtonStyle_Pressed(button)
            } else {
                if (event.action == MotionEvent.ACTION_UP) {
                    setButtonStyle_Released(button)
                    when (target) {
                        Enum.Action.IMPORT -> selectFile()
                        Enum.Action.EXPORT -> onTouchExport()
                    }
                }
            }
            true
        }
    }

    private fun onTouchExport() {
        //check if there is data to export
        val us = TMSession()
        val user = us.userSession
        val records = UserService.getRecords(user.id, "")
        if (records.size > 0) {
            exportData()
        } else {
            val strError = TMLocale.getStringResourceByName("activityimportexport_nodatatoexport")
            val toast = TMToast(this)
            toast.show(strError, Toast.LENGTH_SHORT)
        }
    }

    private fun configureButton(button: TextView) {
        val dpButtonHeight = getButtonsHeight(3) // TMScreen.dp2px(height);
        button.height = dpButtonHeight
        button.textSize = prefsByScreenDensity[3].toFloat()
    }

    /**
     * Imports records data from file into records table
     */
    private fun selectFile() {
        val intent = Intent(Intent.ACTION_GET_CONTENT)
        intent.type = "*/*"
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        activityResultLauncher!!.launch(intent)
    }

    private fun exportData() {
        val recordsData = TMExport.getRecordsData()
        try {
            filename = getFilename()
            val filelocation = TMFile.writeFile(this, filename, recordsData)
            sendExportFileTo(filelocation)
        } catch (e: IOException) {
            val toast = TMToast(this)
            toast.show(e.message, Toast.LENGTH_SHORT)
            ExceptionHandler.logException(e)
        }
    }

    private fun getFilename(): String {
        val formatter = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.ENGLISH)
        val now = Date()
        return "testme_exportfile_" + formatter.format(now) + ".txt"
    }

    private fun sendExportFileTo(attachment: File) {
        val uri = FileProvider.getUriForFile(
            this,
            this.applicationContext.packageName + ".provider",
            attachment
        )
        val i = Intent(Intent.ACTION_SEND)
        i.type = "text/plain"
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        i.putExtra(Intent.EXTRA_EMAIL, arrayOf(""))
        val subject =
            TMLocale.getStringResourceByName("importexport_exporttomail_subject") + " " + filename
        val body = TMLocale.getStringResourceByName("importexport_exporttomail_body")
        i.putExtra(Intent.EXTRA_SUBJECT, subject)
        i.putExtra(Intent.EXTRA_TEXT, body)
        i.putExtra(Intent.EXTRA_STREAM, uri)
        try {
            this.startActivity(Intent.createChooser(i, "Send mail..."))
        } catch (e: ActivityNotFoundException) {
            Toast.makeText(
                this,
                TMLocale.getStringResourceByName("importexport_exporttomail_noemailclients"),
                Toast.LENGTH_SHORT
            )
                .show()
            ExceptionHandler.logException(e)
        }
    }

    companion object {
        private var oData: Intent? = null
    }
}

and "registerForActivityResult" stopped being recognised by compiler.

As I've read here: Android ActivityResult API unresolved reference error registerForActivityResult

the solution passes by importing these next two packages in build.gradle:

api 'androidx.activity:activity-ktx:1.6.0-alpha05'
api 'androidx.fragment:fragment-ktx:1.5.0-rc01'

and then making your activity extend ComponentActivity.

Well, I tried this solutions and it works, "registerforactivityresult" is now recognised, but I cannot inherit from ComponentActivity because all my activities extend BaseActivity, and BaseActivity extends AppCompatActivity, so I cannot extend ComponentActivity in BaseActivity either.

Said that, I'm stuck and don't know how to use registerforactivityresult in Kotlin given my scenario.

Edit 1:

This is my build.gradle support section:

api 'androidx.legacy:legacy-support-v4:1.0.0'
api 'androidx.appcompat:appcompat:1.4.2'
api 'androidx.cardview:cardview:1.0.0'
api 'androidx.recyclerview:recyclerview:1.2.1'
api 'androidx.preference:preference-ktx:1.2.0'

Edit 2:

Not sure if it will be the final solution, but replacing AppCompatActivity by ComponentActivity in the BaseActivity class solved de compilation problem.

Edit 3:

Changing AppCompatActivity by ComponentActivity ended being more an illusion than a solution. If I extend ComponentActivity the registerforactivityresult start working, but FragmentActivities stop compiling because to use SectionsPagerAdapter my class needs to extend FragmentActivity. This is like a vicious circle.

Edit 4:

My complete build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
    compileSdkVersion 32
    def code
    Properties versionProps = new Properties()
    def versionPropsFile = file('version.properties')
    if (versionPropsFile.exists())
        versionProps.load(new FileInputStream(versionPropsFile))
    code = (versionProps['VERSION_CODE'] ?: "0").toInteger()+ 1
    packagingOptions {
        resources {
            pickFirsts += ['META-INF/LICENSE.txt']
            excludes += ['META-INF/NOTICE.md', 'META-INF/LICENSE.md', 'META-INF/INDEX.LIST', 'META-INF/DEPENDENCIES', 'META-INF/io.netty.versions.properties']
        }
    }
    versionProps['VERSION_CODE'] = code.toString()
    versionProps.store(versionPropsFile.newWriter(), null)
    defaultConfig {
        applicationId 'com.xxx.xxx'
        minSdkVersion 26
        targetSdkVersion 32
        multiDexEnabled true
        versionCode code
        versionName "3.0." + code
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    bundle {
        density {
            // Different APKs are generated for devices with different screen densities; true by default.
            enableSplit true
        }
        abi {
            // Different APKs are generated for devices with different CPU architectures; true by default.
            enableSplit true
        }
        language {
            // This is disabled so that the App Bundle does NOT split the APK for each language.
            // We're gonna use the same APK for all languages.
            enableSplit false
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            applicationVariants.all { variant ->
                variant.outputs.all { output ->
                    project.ext { appName = 'xxx' }
                    def newName = 'xxx.apk'
                    outputFileName = new File("./build/", newName)
                }
            }
        }
        debug {
        }
    }
    productFlavors {
    }
    dataBinding{
        enabled = true
    }
    lint {
        abortOnError false
        checkReleaseBuilds false
    }
    namespace 'com.xxx.xxx'
}

dependencies {
    api files('libs/achartengine-1.2.0.jar')
    api 'com.facebook.android:facebook-android-sdk:14.0.0'
    //mail API 16
    implementation 'com.sun.mail:android-mail:1.6.7'
    implementation 'com.sun.mail:android-activation:1.6.7'
    //apache commons text
    implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9'
    //Youtube player
    api 'com.thefinestartist:ytpa:1.2.1'
    //Font Selector List Preference
    //api 'com.vanniktech:vntfontlistpreference:1.0.0'
    //Rate my app
    api 'com.github.hotchemi:android-rate:1.0.1'
    //Support
    api 'androidx.legacy:legacy-support-v4:1.0.0'
    api 'androidx.appcompat:appcompat:1.4.2'
    api 'androidx.cardview:cardview:1.0.0'
    api 'androidx.recyclerview:recyclerview:1.2.1'
    api 'androidx.preference:preference-ktx:1.2.0'
    //Annotation
    api 'androidx.annotation:annotation:1.4.0'
    //AlertDialog
    api 'com.github.d-max:spots-dialog:1.1@aar'
    //glide animated gifs
    api 'com.github.bumptech.glide:glide:4.13.2'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
    //circular score
    api 'com.wssholmes.stark:circular-score:0.1.1'
    api 'com.google.android.material:material:1.6.1'
    api 'com.github.mejdi14:Flat-Dialog-Android:1.0.5'
    //picasso
    api 'com.squareup.picasso:picasso:2.71828'
    //Gson
    implementation 'com.google.code.gson:gson:2.9.0'
    //Stream
    api 'com.annimon:stream:1.2.2'
    api 'androidx.activity:activity-ktx:1.6.0-alpha05'
    api 'androidx.activity:activity-ktx:1.4.0'
    api 'androidx.fragment:fragment-ktx:1.5.0-rc01'
    /*implementation 'jp.wasabeef:picasso-transformations:2.4.0'*/
    //zoom
    //implementation 'com.alexvasilkov:gesture-views:2.8.2'
    //implementation 'com.otaliastudios:zoomlayout:1.8.0'
    //Multidex
    implementation "androidx.multidex:multidex:2.0.1"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation project(':Global')
    implementation project(':DTO')
    implementation project(':Common')
    implementation project(':Background')
    implementation project(':Services')
}

The weird thing is that even the lint error the app builds just fine, but as I'm in the process of migrating wasn't able to test registerForActivityResult yet, but I guess it most likely won't work.

Edit 5:

In this next Google page: https://developer.android.com/jetpack/androidx/releases/activity

says:

"The registerForActivityResult() API has been renamed to rememberLauncherForActivityResult()..."

but the same happens with rememberLauncherForActivityResult, it's not found. I'm completely stuck.

Edit 6:

As far as I've been reading, maybe it's only a "visual" problem, let's say an Android Studio IDE issue, because the app builds fine (I haven't tested the registerForActivityResult yet, though), but anyway I'm fed up with Google.

As time goes by, packages are marked as obsolete sooner everytime, and the amazing thing is that the "replacement" not always work.

For the moment I'll leave my "registerForActivityResult" issue as it is (as I have no choice), but honestly, and in my opinion, Google discourage us, developers, to keep our apps up to date.

hippietrail
  • 15,848
  • 18
  • 99
  • 158
Diego Perez
  • 2,188
  • 2
  • 30
  • 58
  • 2
    "all my activities extend BaseActivity, and BaseActivity extends AppCompatActivity" -- `AppCompatActivity` extends `ComponentActivity`. Make sure that you are up to date with your version of the `appcompat` dependency (and probably drop back to stable versions of `activity-ktx` and `fragment-ktx`). – CommonsWare Jun 19 '22 at 19:27
  • Thank for your reply @CommonsWare. I have this in my build.gradle: api 'androidx.appcompat:appcompat:1.4.2'. Checkmy edits to see my full support section of the build.gradle:) – Diego Perez Jun 19 '22 at 19:43
  • If that's your 'full support section of the build.gradle', then does that mean you _haven't_ included the dependency on `androidx.fragment` as per the other answer? – ianhanniballake Jun 19 '22 at 22:29
  • Thank for your reply @ianhanniballake. Check my Edit 4, I've added my complete build.gradle. – Diego Perez Jun 19 '22 at 22:35
  • That looks like the `build.gradle` for your app module. Is that the module that your `BaseActivity` is declared in? If `BaseActvity` is in the `common` module, for instance, what is the `build.gradle` file for that module? Does _it_ include those same updated dependencies? – ianhanniballake Jun 19 '22 at 23:07
  • I appreciate your time @ianhanniballake. My BaseActivity is in the exact same module, so it uses the same build.gradle file. I keep my build.gradle dependencies always updated, so I'm stuck. – Diego Perez Jun 19 '22 at 23:19
  • I'm stuck with this @CommonsWare. Any other suggestions? I've read that google replaced registerForActivityResult by rememberLauncherForActivityResult, but the same happens. I have all dependencies up to date and with stable versions :s – Diego Perez Jul 04 '22 at 23:13
  • @DiegoPerez: "Any other suggestions?" -- examine existing projects that already use `registerForActivityResult()`. Four projects in [this repo](https://gitlab.com/commonsguy/cw-jetpack-kotlin) do, for example. – CommonsWare Jul 04 '22 at 23:15

1 Answers1

3

I've ended up creating an extension for this function:

fun <I, O> Activity.registerForActivityResult(
    contract: ActivityResultContract<I, O>,
    callback: ActivityResultCallback<O>
) = (this as ComponentActivity).registerForActivityResult(contract, callback)

It seems there is an open bug for AndroidStudio: Bug: unable to find registerForActivityResult on large project despite having the needed dependencies

Ionut Negru
  • 6,186
  • 4
  • 48
  • 78