Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit fbb73e2d authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #8074 from thunderbird/ViewIntentFinder

Extract `ViewIntentFinder` from `AttachmentController`
parents 054accc3 63bdbf51
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@
        </intent>

        <intent>
            <!-- Used by AttachmentController to find the best Intent to view an attachment -->
            <!-- Used by ViewIntentFinder to find the best Intent to view an attachment -->
            <action android:name="android.intent.action.VIEW" />
            <data
                android:mimeType="*/*"
+6 −76
Original line number Diff line number Diff line
@@ -4,24 +4,20 @@ package com.fsck.k9.ui.messageview;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.annotation.WorkerThread;
import android.widget.Toast;

import androidx.annotation.WorkerThread;
import app.k9mail.legacy.account.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.controller.MessagingController;
import app.k9mail.legacy.message.controller.SimpleMessagingListener;
import com.fsck.k9.helper.MimeTypeUtil;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.AttachmentViewInfo;
@@ -38,6 +34,7 @@ public class AttachmentController {
    private final MessagingController controller;
    private final MessageViewFragment messageViewFragment;
    private final AttachmentViewInfo attachment;
    private final ViewIntentFinder viewIntentFinder;


    AttachmentController(Context context, MessagingController controller, MessageViewFragment messageViewFragment,
@@ -46,6 +43,7 @@ public class AttachmentController {
        this.controller = controller;
        this.messageViewFragment = messageViewFragment;
        this.attachment = attachment;
        viewIntentFinder = new ViewIntentFinder(context);
    }

    public void viewAttachment() {
@@ -131,64 +129,14 @@ public class AttachmentController {

    @WorkerThread
    private Intent getBestViewIntent() {
        Uri intentDataUri;
        try {
            intentDataUri = AttachmentTempFileProvider.createTempUriForContentUri(context, attachment.internalUri);
            Uri intentDataUri = AttachmentTempFileProvider.createTempUriForContentUri(context, attachment.internalUri);

            return viewIntentFinder.getBestViewIntent(intentDataUri, attachment.displayName, attachment.mimeType);
        } catch (IOException e) {
            Timber.e(e, "Error creating temp file for attachment!");
            return null;
        }

        String displayName = attachment.displayName;
        String inferredMimeType = MimeTypeUtil.getMimeTypeByExtension(displayName);

        IntentAndResolvedActivitiesCount resolvedIntentInfo;
        String mimeType = attachment.mimeType;
        if (MimeTypeUtil.isDefaultMimeType(mimeType)) {
            resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, inferredMimeType);
        } else {
            resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, mimeType);
            if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(mimeType)) {
                resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, inferredMimeType);
            }
        }

        if (!resolvedIntentInfo.hasResolvedActivities()) {
            resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE);
        }

        return resolvedIntentInfo.getIntent();
    }

    private IntentAndResolvedActivitiesCount getViewIntentForMimeType(Uri contentUri, String mimeType) {
        Intent contentUriIntent = createViewIntentForAttachmentProviderUri(contentUri, mimeType);
        int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent);

        return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
    }

    private Intent createViewIntentForAttachmentProviderUri(Uri contentUri, String mimeType) {
        Uri uri = AttachmentTempFileProvider.getMimeTypeUri(contentUri, mimeType);

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, mimeType);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        addUiIntentFlags(intent);

        return intent;
    }

    private void addUiIntentFlags(Intent intent) {
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    }

    private int getResolvedIntentActivitiesCount(Intent intent) {
        PackageManager packageManager = context.getPackageManager();

        List<ResolveInfo> resolveInfos =
                packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

        return resolveInfos.size();
    }

    private void displayAttachmentNotSavedMessage() {
@@ -200,24 +148,6 @@ public class AttachmentController {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    }

    private static class IntentAndResolvedActivitiesCount {
        private Intent intent;
        private int activitiesCount;

        IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) {
            this.intent = intent;
            this.activitiesCount = activitiesCount;
        }

        public Intent getIntent() {
            return intent;
        }

        public boolean hasResolvedActivities() {
            return activitiesCount > 0;
        }
    }

    private class ViewAttachmentAsyncTask extends AsyncTask<Void, Void, Intent> {

        @Override
+73 −0
Original line number Diff line number Diff line
package com.fsck.k9.ui.messageview

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.annotation.WorkerThread
import com.fsck.k9.helper.MimeTypeUtil
import com.fsck.k9.helper.MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE
import com.fsck.k9.provider.AttachmentTempFileProvider

/**
 * Tries to find an [Intent.ACTION_VIEW] Intent that can be used to view an attachment.
 */
internal class ViewIntentFinder(private val context: Context) {
    @WorkerThread
    fun getBestViewIntent(contentUri: Uri, displayName: String?, mimeType: String): Intent {
        val inferredMimeType = MimeTypeUtil.getMimeTypeByExtension(displayName)

        var resolvedIntentInfo: QueryIntentResult
        if (MimeTypeUtil.isDefaultMimeType(mimeType)) {
            resolvedIntentInfo = getViewIntentForMimeType(contentUri, inferredMimeType)
        } else {
            resolvedIntentInfo = getViewIntentForMimeType(contentUri, mimeType)
            if (resolvedIntentInfo.hasNoResolvedActivities() && inferredMimeType != mimeType) {
                resolvedIntentInfo = getViewIntentForMimeType(contentUri, inferredMimeType)
            }
        }

        if (resolvedIntentInfo.hasNoResolvedActivities()) {
            resolvedIntentInfo = getViewIntentForMimeType(contentUri, DEFAULT_ATTACHMENT_MIME_TYPE)
        }

        return resolvedIntentInfo.intent
    }

    private fun getViewIntentForMimeType(contentUri: Uri, mimeType: String): QueryIntentResult {
        val intent = createViewIntentForAttachmentProviderUri(contentUri, mimeType)
        val activitiesCount = getResolvedIntentActivitiesCount(intent)

        return QueryIntentResult(intent, activitiesCount)
    }

    private fun createViewIntentForAttachmentProviderUri(contentUri: Uri, mimeType: String): Intent {
        val uri = AttachmentTempFileProvider.getMimeTypeUri(contentUri, mimeType)

        return Intent(Intent.ACTION_VIEW).apply {
            setDataAndType(uri, mimeType)
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            addUiIntentFlags()
        }
    }

    private fun Intent.addUiIntentFlags() {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
    }

    private fun getResolvedIntentActivitiesCount(intent: Intent): Int {
        val packageManager = context.packageManager
        val resolveInfos = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)

        return resolveInfos.size
    }
}

private class QueryIntentResult(
    val intent: Intent,
    private val activitiesCount: Int,
) {
    fun hasNoResolvedActivities(): Boolean {
        return activitiesCount == 0
    }
}
+142 −0
Original line number Diff line number Diff line
package com.fsck.k9.ui.messageview

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.ProviderInfo
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import app.k9mail.core.android.testing.RobolectricTest
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.prop
import com.fsck.k9.provider.AttachmentTempFileProvider
import kotlin.test.Test
import org.junit.Before
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf

class ViewIntentFinderTest : RobolectricTest() {
    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val attachmentTempFileProviderAuthority = "${context.packageName}.tempfileprovider"
    private val viewIntentFinder = ViewIntentFinder(context)

    @Before
    fun setUp() {
        // The AttachmentTempFileProvider methods called by ViewIntentFinder require the AUTHORITY property to be
        // set. The most robust way to accomplish this is to properly initialize the content provider.
        initializeAttachmentTempFileProvider()
    }

    @Test
    fun `provided non-default content type should be preferred`() {
        addViewerAppFor(mimeType = "application/pdf")
        addViewerAppFor(mimeType = "application/octet-stream")
        val contentUri = "content://$attachmentTempFileProviderAuthority/id".toUri()
        val displayName = "document.pdf"
        val mimeType = "application/pdf"

        val result = viewIntentFinder.getBestViewIntent(contentUri, displayName, mimeType)

        assertThat(result).all {
            prop(Intent::getAction).isEqualTo(Intent.ACTION_VIEW)
            prop(Intent::getData).isEqualTo(
                "content://$attachmentTempFileProviderAuthority/id?mime_type=application%2Fpdf".toUri(),
            )
            prop(Intent::getType).isEqualTo("application/pdf")
        }
    }

    @Test
    fun `inferred content type should be preferred over provided default content type`() {
        addViewerAppFor(mimeType = "application/pdf")
        addViewerAppFor(mimeType = "application/octet-stream")
        val contentUri = "content://$attachmentTempFileProviderAuthority/id".toUri()
        val displayName = "document.pdf"
        val mimeType = "application/octet-stream"

        val result = viewIntentFinder.getBestViewIntent(contentUri, displayName, mimeType)

        assertThat(result).all {
            prop(Intent::getAction).isEqualTo(Intent.ACTION_VIEW)
            prop(Intent::getData).isEqualTo(
                "content://$attachmentTempFileProviderAuthority/id?mime_type=application%2Fpdf".toUri(),
            )
            prop(Intent::getType).isEqualTo("application/pdf")
        }
    }

    @Test
    fun `inferred content type should be used when no app is installed for provided content type`() {
        addViewerAppFor(mimeType = "text/plain")
        addViewerAppFor(mimeType = "application/octet-stream")
        val contentUri = "content://$attachmentTempFileProviderAuthority/id".toUri()
        val displayName = "document.txt"
        val mimeType = "text/fancy-format"

        val result = viewIntentFinder.getBestViewIntent(contentUri, displayName, mimeType)

        assertThat(result).all {
            prop(Intent::getAction).isEqualTo(Intent.ACTION_VIEW)
            prop(Intent::getData).isEqualTo(
                "content://$attachmentTempFileProviderAuthority/id?mime_type=text%2Fplain".toUri(),
            )
            prop(Intent::getType).isEqualTo("text/plain")
        }
    }

    @Test
    fun `fall back to default content type when no app is installed for provided content type`() {
        addViewerAppFor(mimeType = "application/octet-stream")
        val contentUri = "content://$attachmentTempFileProviderAuthority/id".toUri()
        val displayName = "document.pdf"
        val mimeType = "application/pdf"

        val result = viewIntentFinder.getBestViewIntent(contentUri, displayName, mimeType)

        assertThat(result).all {
            prop(Intent::getAction).isEqualTo(Intent.ACTION_VIEW)
            prop(Intent::getData).isEqualTo(
                "content://$attachmentTempFileProviderAuthority/id?mime_type=application%2Foctet-stream".toUri(),
            )
            prop(Intent::getType).isEqualTo("application/octet-stream")
        }
    }

    private fun initializeAttachmentTempFileProvider() {
        val info = ProviderInfo().apply {
            authority = attachmentTempFileProviderAuthority
            grantUriPermissions = true
        }
        Robolectric.buildContentProvider(AttachmentTempFileProvider::class.java).create(info)
    }

    private var packageCounter = 1

    private fun addViewerAppFor(mimeType: String) {
        val viewerPackageName = "test.viewerapp.$packageCounter"
        val viewerActivityName = "$viewerPackageName.activity"
        packageCounter++

        val packageManager = shadowOf(context.packageManager)

        packageManager.installPackage(
            PackageInfo().apply {
                packageName = viewerPackageName
            },
        )

        val viewerActivityComponentName = ComponentName(viewerPackageName, viewerActivityName)
        packageManager.addActivityIfNotPresent(viewerActivityComponentName)

        val intentFilter = IntentFilter(Intent.ACTION_VIEW).apply {
            addDataType(mimeType)
            addDataScheme("content")
            addCategory(Intent.CATEGORY_DEFAULT)
        }
        packageManager.addIntentFilterForActivity(viewerActivityComponentName, intentFilter)
    }
}