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

Commit 142a344c authored by Wenyi Wang's avatar Wenyi Wang
Browse files

Save contacts to local file and share

We extend ExportVCardActivity in order to reuse VCarcService
to write contacts data to vcf file in background. This change
gets rid of TransactionTooLargeException and provides better
user experience, since we write data in background.

The current UX design is: user will receive Toast message
once export file is ready, and then user needs to tap the
notification to share contacts.

Bug 22083005

Change-Id: I7d7142f3037b1a0647d185d477365df8f2994271
(cherry picked from commit e6c7494bbafe1bef1187245510b1ec0beba6ce10)
parent 1f82861d
Loading
Loading
Loading
Loading
+8 −53
Original line number Diff line number Diff line
@@ -25,8 +25,6 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.telephony.SubscriptionInfo;
@@ -39,7 +37,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.Toast;

import com.android.contacts.common.R;
import com.android.contacts.common.compat.CompatUtils;
@@ -49,9 +46,9 @@ import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.util.AccountSelectionUtil;
import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
import com.android.contacts.common.util.ImplicitIntentsUtil;
import com.android.contacts.common.vcard.ExportVCardActivity;
import com.android.contacts.common.vcard.VCardCommonArguments;
import com.android.contacts.common.vcard.ShareVCardActivity;
import com.android.contacts.commonbind.analytics.AnalyticsUtil;

import java.util.List;
@@ -196,7 +193,8 @@ public class ImportExportDialogFragment extends DialogFragment
                    }
                    case R.string.export_to_vcf_file: {
                        dismissDialog = true;
                        Intent exportIntent = new Intent(getActivity(), ExportVCardActivity.class);
                        final Intent exportIntent = new Intent(
                                getActivity(), ExportVCardActivity.class);
                        exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,
                                callingActivity);
                        getActivity().startActivity(exportIntent);
@@ -204,7 +202,11 @@ public class ImportExportDialogFragment extends DialogFragment
                    }
                    case R.string.share_contacts: {
                        dismissDialog = true;
                        doShareContacts();
                        final Intent exportIntent = new Intent(
                                getActivity(), ShareVCardActivity.class);
                        exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,
                                callingActivity);
                        getActivity().startActivity(exportIntent);
                        break;
                    }
                    default: {
@@ -226,53 +228,6 @@ public class ImportExportDialogFragment extends DialogFragment
                .create();
    }

    private void doShareContacts() {
        try {
            // TODO move the query into a loader and do this in a background thread
            final Cursor cursor;
            if (mExportMode == EXPORT_MODE_FAVORITES) {
                cursor = getActivity().getContentResolver().query(Contacts.CONTENT_STREQUENT_URI,
                        LOOKUP_PROJECTION, null, null,
                        Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC");
            } else { // EXPORT_MODE_ALL_CONTACTS
                cursor = getActivity().getContentResolver().query(Contacts.CONTENT_URI,
                        LOOKUP_PROJECTION, Contacts.IN_VISIBLE_GROUP + "!=0", null, null);
            }
            if (cursor != null) {
                try {
                    if (!cursor.moveToFirst()) {
                        Toast.makeText(getActivity(), R.string.no_contact_to_share,
                                Toast.LENGTH_SHORT).show();
                        return;
                    }

                    StringBuilder uriListBuilder = new StringBuilder();
                    int index = 0;
                    do {
                        if (index != 0)
                            uriListBuilder.append(':');
                        uriListBuilder.append(cursor.getString(0));
                        index++;
                    } while (cursor.moveToNext());
                    Uri uri = Uri.withAppendedPath(
                            Contacts.CONTENT_MULTI_VCARD_URI,
                            Uri.encode(uriListBuilder.toString()));

                    final Intent intent = new Intent(Intent.ACTION_SEND);
                    intent.setType(Contacts.CONTENT_VCARD_TYPE);
                    intent.putExtra(Intent.EXTRA_STREAM, uri);
                    ImplicitIntentsUtil.startActivityOutsideApp(getActivity(), intent);
                } finally {
                    cursor.close();
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Sharing contacts failed", e);
            Toast.makeText(getContext(), R.string.share_contacts_failure,
                    Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Handle "import from SIM" and "import from SD".
     *
+53 −5
Original line number Diff line number Diff line
@@ -22,10 +22,13 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContactsEntity;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.android.contacts.common.R;
import com.android.vcard.VCardComposer;
@@ -53,10 +56,21 @@ public class ExportProcessor extends ProcessorBase {
    private final int mJobId;
    private final String mCallingActivity;


    private volatile boolean mCanceled;
    private volatile boolean mDone;

    private final int SHOW_READY_TOAST = 1;
    private final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.arg1 == SHOW_READY_TOAST) {
                // This message is long, so we set the duration to LENGTH_LONG.
                Toast.makeText(mService,
                        R.string.exporting_vcard_finished_toast, Toast.LENGTH_LONG).show();
            }

        }
    };

    public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId,
            String callingActivity) {
        mService = service;
@@ -198,10 +212,22 @@ public class ExportProcessor extends ProcessorBase {

            successful = true;
            final String filename = ExportVCardActivity.getOpenableUriDisplayName(mService, uri);
            // If it is a local file (i.e. not a file from Drive), we need to allow user to share
            // the file by pressing the notification; otherwise, it would be a file in Drive, we
            // don't need to enable this action in notification since the file is already uploaded.
            if (isLocalFile(uri)) {
                final Message msg = handler.obtainMessage();
                msg.arg1 = SHOW_READY_TOAST;
                handler.sendMessage(msg);
                doFinishNotificationWithShareAction(
                        mService.getString(R.string.exporting_vcard_finished_title_fallback),
                        mService.getString(R.string.touch_to_share_contacts), uri);
            } else {
                final String title = filename == null
                        ? mService.getString(R.string.exporting_vcard_finished_title_fallback)
                        : mService.getString(R.string.exporting_vcard_finished_title, filename);
                doFinishNotification(title, null);
            }
        } finally {
            if (composer != null) {
                composer.terminate();
@@ -217,6 +243,11 @@ public class ExportProcessor extends ProcessorBase {
        }
    }

    private boolean isLocalFile(Uri uri) {
        final String authority = uri.getAuthority();
        return mService.getString(R.string.contacts_file_provider_authority).equals(authority);
    }

    private String translateComposerError(String errorMessage) {
        final Resources resources = mService.getResources();
        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
@@ -265,6 +296,23 @@ public class ExportProcessor extends ProcessorBase {
                mJobId, notification);
    }

    /**
     * Pass intent with ACTION_SEND to notification so that user can press the notification to
     * share contacts.
     */
    private void doFinishNotificationWithShareAction(final String title, final String
            description, Uri uri) {
        if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
        final Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType(Contacts.CONTENT_VCARD_TYPE);
        intent.putExtra(Intent.EXTRA_STREAM, uri);
        final Notification notification =
                NotificationImportExportListener.constructFinishNotificationWithFlags(
                        mService, title, description, intent, Intent.FLAG_ACTIVITY_NEW_TASK);
        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
                mJobId, notification);
    }

    @Override
    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
        if (DEBUG) Log.d(LOG_TAG, "received cancel request");
+7 −7
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ import com.android.contacts.common.activity.RequestImportVCardPermissionsActivit
public class ExportVCardActivity extends Activity implements ServiceConnection,
        DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
    private static final String LOG_TAG = "VCardExport";
    private static final boolean DEBUG = VCardService.DEBUG;
    protected static final boolean DEBUG = VCardService.DEBUG;
    private static final int REQUEST_CREATE_DOCUMENT = 100;

    /**
@@ -53,7 +53,7 @@ public class ExportVCardActivity extends Activity implements ServiceConnection,
     *
     * Should be touched inside synchronized block.
     */
    private boolean mConnected;
    protected boolean mConnected;

    /**
     * True when users need to do something and this Activity should not disconnect from
@@ -62,7 +62,7 @@ public class ExportVCardActivity extends Activity implements ServiceConnection,
     */
    private volatile boolean mProcessOngoing = true;

    private VCardService mService;
    protected VCardService mService;
    private static final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();

    // String for storing error reason temporarily.
@@ -105,9 +105,9 @@ public class ExportVCardActivity extends Activity implements ServiceConnection,
        if (requestCode == REQUEST_CREATE_DOCUMENT) {
            if (resultCode == Activity.RESULT_OK && mService != null &&
                    data != null && data.getData() != null) {
                final Uri mTargetFileName = data.getData();
                if (DEBUG) Log.d(LOG_TAG, "exporting to " + mTargetFileName);
                final ExportRequest request = new ExportRequest(mTargetFileName);
                final Uri targetFileName = data.getData();
                if (DEBUG) Log.d(LOG_TAG, "exporting to " + targetFileName);
                final ExportRequest request = new ExportRequest(targetFileName);
                // The connection object will call finish().
                mService.handleExportRequest(request, new NotificationImportExportListener(
                        ExportVCardActivity.this));
@@ -223,7 +223,7 @@ public class ExportVCardActivity extends Activity implements ServiceConnection,
        return null;
    }

    private synchronized void unbindAndFinish() {
    protected synchronized void unbindAndFinish() {
        if (mConnected) {
            unbindService(this);
            mConnected = false;
+11 −4
Original line number Diff line number Diff line
@@ -160,9 +160,7 @@ public class NotificationImportExportListener implements VCardImportExportListen
    public void onExportProcessed(ExportRequest request, int jobId) {
        final String displayName = ExportVCardActivity.getOpenableUriDisplayName(mContext,
                request.destUri);
        final String message = displayName == null
                ? mContext.getString(R.string.vcard_export_will_start_message_fallback)
                : mContext.getString(R.string.vcard_export_will_start_message, displayName);
        final String message = mContext.getString(R.string.contacts_export_will_start_message);

        mHandler.obtainMessage(0, message).sendToTarget();
        final Notification notification =
@@ -269,6 +267,14 @@ public class NotificationImportExportListener implements VCardImportExportListen
     */
    /* package */ static Notification constructFinishNotification(
            Context context, String title, String description, Intent intent) {
        return constructFinishNotificationWithFlags(context, title, description, intent, 0);
    }

    /**
     * @param flags use FLAG_ACTIVITY_NEW_TASK to set it as new task, to get rid of cached files.
     */
    /* package */ static Notification constructFinishNotificationWithFlags(
            Context context, String title, String description, Intent intent, int flags) {
        return new NotificationCompat.Builder(context)
                .setAutoCancel(true)
                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
@@ -279,7 +285,8 @@ public class NotificationImportExportListener implements VCardImportExportListen
                // Restrict the intent to this app to make sure that no other app can steal this
                // pending-intent b/19296918.
                .setContentIntent(PendingIntent.getActivity(context, 0,
                        (intent != null ? intent : new Intent(context.getPackageName(), null)), 0))
                        (intent != null ? intent : new Intent(context.getPackageName(), null)),
                        flags))
                .getNotification();
    }

+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.contacts.common.vcard;

import android.content.ComponentName;
import android.net.Uri;
import android.os.IBinder;
import android.support.v4.content.FileProvider;
import android.util.Log;

import com.android.contacts.common.R;

import java.io.File;
import java.io.IOException;

/**
 * This activity connects to VCardService, creates a .vcf file in cache directory and send export
 * request with the file URI so as to write contacts data to the file in background.
 */
public class ShareVCardActivity extends ExportVCardActivity {
    private static final String LOG_TAG = "VCardShare";
    private final String EXPORT_FILE_PREFIX = "export_";
    private final long A_DAY_IN_MILLIS = 1000 * 60 * 60 * 24;

    @Override
    public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
        if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
        mConnected = true;
        mService = ((VCardService.MyBinder) binder).getService();

        clearExportFiles();

        final File file = getLocalFile();
        try {
            file.createNewFile();
        } catch (IOException e) {
            Log.e(LOG_TAG, "Failed to create .vcf file, because: " + e);
            unbindAndFinish();
            return;
        }

        final Uri contentUri = FileProvider.getUriForFile(this,
                getString(R.string.contacts_file_provider_authority), file);
        if (DEBUG) Log.d(LOG_TAG, "exporting to " + contentUri);

        final ExportRequest request = new ExportRequest(contentUri);
        // The connection object will call finish().
        mService.handleExportRequest(request, new NotificationImportExportListener(
                ShareVCardActivity.this));
        unbindAndFinish();
    }

    /**
     * Delete the files (that are untouched for more than 1 day) in the cache directory.
     * We cannot rely on VCardService to delete export files because it will delete export files
     * right after finishing writing so no files could be shared. Therefore, our approach to
     * deleting export files is:
     * 1. put export files in cache directory so that Android may delete them;
     * 2. manually delete the files that are older than 1 day when service is connected.
     */
    private void clearExportFiles() {
        for (File file : getCacheDir().listFiles()) {
            final long ageInMillis = System.currentTimeMillis() - file.lastModified();
            if (file.getName().startsWith(EXPORT_FILE_PREFIX) && ageInMillis > A_DAY_IN_MILLIS) {
                file.delete();
            }
        }
    }

    private File getLocalFile() {
        int cache_index = 0;
        String localFilename;
        File file;
        while (true) {
            localFilename = EXPORT_FILE_PREFIX + cache_index + ".vcf";
            file = new File(getCacheDir(), localFilename);
            if (!file.exists()) {
                break;
            }
            if (cache_index == Integer.MAX_VALUE) {
                throw new RuntimeException("Exceeded cache limit");
            }
            cache_index++;
        }
        return file;
    }
}
 No newline at end of file