Pull to refresh

Как я одной кнопкой шарил разные данные в Android приложении

Reading time 9 min
Views 10K


Как-то раз передо мной встала задача добавить экспорт в календарь к уже написанному экспорту обычных текстовых данных через ShareActionProvider кнопку. Сходу нашлись несколько вариантов, каждый из которых мне по каким-либо причинам не подходил.

SO1 предлагал мне изменить MIME тип с «text/plain» на "*/*", чтобы охватить большее число установленных приложений. Это добавило очень много лишних приложений, и нужные терялись в море ненужных. Были предложения использовать библиотеки, также, SO предлагал создать свой собственный Intent Chooser, и в нём реализовать логику выбора, какие данные надо экспортировать. Мне не хотелось использовать диалоговое окно только для того, чтобы можно было выбирать из нескольких типов приложений — и я решил разобраться с исходниками ShareActionProvider.

Копание в исходниках:


Первым делом, мой взгляд упал на метод setShareIntent, который принимал собранный Intent с данными для экспорта. А что, если можно сделать универсальный intent, спросил я себя и ринулся искать, как объединить два интента в один, да ещё и с разными действиями (Intent.ACTION_INSERT и Intent.ACTION_SEND). Ни одно решение, что я нашёл (не так уж и глубоко я копал, если честно), поэтому я решил подсмотреть, что делается под капотом класса ShareActionProvider. Забегая вперёд, скажу, что получая от гугла исходники2, находя классы, работающие с нашим интентом, и повторяя шаги 1 и 2 несколько раз я выяснил, что всем заведуют три класса: собственно, ShareActionProvider, ActivityChooserView и ActivityChooserModel. Последние два отвечают за выбор нужных для нашего интента приложений, создания выпадающего списка и обработки выбора списка.

Само решение проблемы я решил начать с изменения типа данных, которые я буду передавать в setShareIntent(). По логике вещей, если я хочу экспортировать больше разных данных — мне нужны больше интентов и, следовательно, первое решение, которое приходит в голову — это использовать массив:

    public void setShareIntent(Intent shareIntent) {
        if (shareIntent != null) {
            final String action = shareIntent.getAction();
            if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
            }
        }
        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
        dataModel.setIntent(shareIntent);
    }


меняем на:

    public void setShareIntent(Intent[] shareIntents) { // Изменили тип на массив
        for (Intent intent : shareIntents) { // Добавили прохождение по всему массиву
            if (intent != null) {
                final String action = intent.getAction();
                if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
                }
            }
        }
        CustomActivityChooserModel dataModel = CustomActivityChooserModel.get(mContext, mShareHistoryFileName); // Заменили класс ActivityChooserModel на наш, самодельный
        dataModel.setIntent(shareIntents); // И передаём массив в dataModel
    }


Первый шаг пройден, первый метод изменён, идём дальше по цепочке. Следующая проблема проявилась в объекте dataModel. Он (или она, модель) никак не хочет брать наш массив. Что поделать, идём внутрь ActivityChooserModel.get() и смотрим, что мы можем изменить там:

    public static CustomActivityChooserModel get(Context context, String historyFileName) {
        synchronized (sRegistryLock) {
            CustomActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
            if (dataModel == null) {
                dataModel = new CustomActivityChooserModel(context, historyFileName);
                sDataModelRegistry.put(historyFileName, dataModel);
            }
            return dataModel;
        }
    }


На самом деле, в этом методе мы изменили только название класса с ActivityChooserModel на наше. Отсюда наш путь идёт через sDataModelRegistry в метод get(), но sDataModelRegistry — это всего лишь множество Map, которое возвращает нам объект типа ActivityChooserModel. Замкнутый круг. Выходим из мысленного цикла и пробуем другой подход -> если dataModel — это объект типа ActivityChooserModel, значит, у него есть метод setIntent(). Нам остаётся (слишком наивно) только изменить тип его входного параметра на массив:

    public void setIntent(Intent[] intents) { // Меняем на массив и чуть правим код
        synchronized (mInstanceLock) {
            if (mIntents == intents) { // Попутно надо поменять intent mIntent на Intent[] mIntents
                return;
            }
            mIntents = intents;
            mReloadActivities = true;
            ensureConsistentState();
        }
    }

    // Надо будет поправить несколько методов после изменения mIntent на mIntents
    // Эти методы: getIntent(), chooseActivity(), sortActivitiesIfNeeded(), loadActivitiesIfNeeded()
    // getIntent() изменить проще простого, поэтому его я опущу
    // До chooseActivity() мы ещё дойдём. Его нам надо будет изменить больше, чем просто поменяв mIntent на mIntents


Продолжаем раскопки. Добавляем в нашу углеродную форму стека ещё один метод ensureConsistentState(), и погружаемся в него с головой для правки и находим два метода — loadActivitiesIfNeeded() и sortActivitiesIfNeeded(). Это как раз те, которые нам надо поправить. Мысленно надеемся, что тенденция не продолжится, и мы не закончим с шестнадцатью методами на пятом шаге.

Начинаем с первого метода:

    private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();

    /* ... */ // - это не смайлик

    private boolean loadActivitiesIfNeeded() {
        if (mReloadActivities && mIntent != null) {
            mReloadActivities = false;
            mActivities.clear();
            List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentActivities(mIntent, 0);
            final int resolveInfoCount = resolveInfos.size();
            for (int i = 0; i < resolveInfoCount; i++) {
                ResolveInfo resolveInfo = resolveInfos.get(i);
                mActivities.add(new ActivityResolveInfo(resolveInfo));
            }
            return true;
        }
        return false;
    }


меняем на:

    private final LinkedHashMap<Intent, ArrayList<ActivityResolveInfo>> mActivities = new LinkedHashMap<Intent, ArrayList<ActivityResolveInfo>>();
    // Во-первых, понимаем, что объект mActivities нам следует изменить, чтобы знать, к какому интенту относится та или иная активити (ту мэни инглиш вордс. неверзелесс, продолжаем-с)

    /* ... */

    private boolean loadActivitiesIfNeeded() {
        if (mReloadActivities && mIntents != null) {
            mReloadActivities = false;
            mActivities.clear();

            for (Intent intent : mIntents) { // Добавляем цикл по массиву
                List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentActivities(intent, 0);
                ArrayList<ActivityResolveInfo> activityResolveInfos = new ArrayList<>(); // И создаём ArrayList с активити для каждого интента
                final int resolveInfoCount = resolveInfos.size();
                for (int i = 0; i < resolveInfoCount; i++) {
                    ResolveInfo resolveInfo = resolveInfos.get(i);
                    activityResolveInfos.add(new ActivityResolveInfo(resolveInfo));
                }
                mActivities.put(intent, activityResolveInfos); // Добавляем в множество, где ключ - интент. Теперь у нас есть разделение активит по интентам, и теперь будет проще их использовать
            }
            return true;
        }
        return false;
    }


Продвигаемся к методу сортировки, тут всё просто, добавляем цикл по массиву вместо единичного элемента. Теперь мы знаем, что всё у нас хранится в множестве, поэтому никаких входных параметров метода не требуется:

    private boolean sortActivitiesIfNeeded() {
        if (mActivitySorter != null && mIntents != null && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
            for (Intent intent : mIntents) { // Всего-то добавить цикл
                mActivitySorter.sort(intent, mActivities.get(intent), Collections.unmodifiableList(mHistoricalRecords));
            } // ⸮ Не забывайте закрывать циклы и другие блоки. Иначе код не скомпилируется ⸮
            return true;
        }
        return false;
    }


Чистим код за собой


Осматриваемся. У нас появились ещё методы, которые несогласны с нашими изменениями: getActivity(), getActivityIndex(), всё тот же chooseActivity(), уже с новой ошибкой, дальше — getDefaultActivity() и setDefaultActivity(). Посмотрев ближе — видим, что они ругаются только на изменения типа mActivities с ArrayList на LinkedHashMap, делов то:

Добавим метод для получения ActivityResolveInfo по индексу

    /**
     * Gets an activity resolve info at a given index.
     *
     * @return The activity resolve info.
     * @see ActivityResolveInfo
     * @see #setIntent(Intent[])
     */
    private ActivityResolveInfo getActivityResolveInfo(int index) {
        synchronized (mInstanceLock) {
            ensureConsistentState();

            Collection<ArrayList<ActivityResolveInfo>> activitiesValues = mActivities.values();

            ArrayList<ActivityResolveInfo> activitiesList = new ArrayList<>();

            for (ArrayList<ActivityResolveInfo> list : activitiesValues) {
                activitiesList.addAll(list);
            }

            return activitiesList.get(index);
        }
    }


Этот метод нам ещё поможет. После этого меняем:

    public ResolveInfo getActivity(int index) {
        synchronized (mInstanceLock) {
            ensureConsistentState();
            return mActivities.get(index).resolveInfo;
        }
    }


На:

    public ResolveInfo getActivity(int index) {
        return getActivityResolveInfo(index).resolveInfo;
    }


Всё просто…

Вспоминаем, что уже сделано, а что осталось:
  • getIntent()
  • sortActivitiesIfNeeded()
  • loadActivitiesIfNeeded()
  • getActivity()
  • getDefaultActivity()
  • setDefaultActivity()
  • getActivityIndex()
  • chooseActivity()


Займёмся дефолтными активити. Надо приспособить их для использования Map:

В методе setDefaultActivity() мы только берём ArrayList по первому ключу:
    public void setDefaultActivity(int index) {

        // Неизменный код

        // Старый код
        // ActivityResolveInfo newDefaultActivity = mActivities.get(index);
        // ActivityResolveInfo oldDefaultActivity = mActivities.get(0);

        // Новый код
        ActivityResolveInfo newDefaultActivity = mActivities.get(mIntents[0]).get(index);
        ActivityResolveInfo oldDefaultActivity = mActivities.get(mIntents[0]).get(0);

        // Тоже неизменный код


Что касается getDefaultActivity():

    public ResolveInfo getDefaultActivity() {
        synchronized (mInstanceLock) {
            ensureConsistentState();
            if (!mActivities.isEmpty()) {
                return mActivities.get(0).resolveInfo;
            }
        }
        return null;
    }


Нам надо получить первый элемент первого ключа:

    public ResolveInfo getDefaultActivity() {
        synchronized (mInstanceLock) {
            ensureConsistentState();
            if (!mActivities.isEmpty()) {
                for (ArrayList<ActivityResolveInfo> arrayList : mActivities.values()) { // Входим в цикл
                    if (!arrayList.isEmpty()) {
                        return arrayList.get(0).resolveInfo; // Если массив не пустой - возвращаем ResolveInfo первого элемента
                    }
                }
            }
        }
        return null; // Ну и никогда не лишним вернуть null
    }


Остаются два метода: getActivityIndex() и chooseActivity().

Чтобы получить индекс активити — нам надо взять строку

            List<ActivityResolveInfo> activities = mActivities;
            final int activityCount = activities.size();


И расписать всё то же, только с несколькими ArrayList, которые мы держим в mActivities:

            HashMap<Intent, ArrayList<ActivityResolveInfo>> activities = mActivities;

            Collection<ArrayList<ActivityResolveInfo>> activitiesValues = activities.values();
            ArrayList<ActivityResolveInfo> activitiesList = new ArrayList<>();
            for (ArrayList<ActivityResolveInfo> list : activitiesValues) {
                activitiesList.addAll(list); // Создаём новый ArrayList и добавляем туда все активити из всех массивов циклом
            }

            final int activityCount = activitiesList.size();


Теперь нам надо выбирать активити, изменений немало, поэтому приведу весь метод, простите за кучу кода :-(

Старый метод:
    public Intent chooseActivity(int index) {
        synchronized (mInstanceLock) {
            if (mIntent == null) {
                return null;
            }
            ensureConsistentState();
            ActivityResolveInfo chosenActivity = mActivities.get(index);
            ComponentName chosenName =
                    new ComponentName(chosenActivity.resolveInfo.activityInfo.packageName, chosenActivity.resolveInfo.activityInfo.name);

            Intent choiceIntent = new Intent(mIntent);

            // Весь оставшийся код не меняем
        }
    }


The new метод:
    public Intent chooseActivity(int index) {
        synchronized (mInstanceLock) {
            if (mIntents == null) {
                return null;
            }
            ensureConsistentState();
            ActivityResolveInfo chosenActivity = getActivityResolveInfo(index); // Используем написанный нами вспомогательный метод
            ComponentName chosenName =
                    new ComponentName(chosenActivity.resolveInfo.activityInfo.packageName, chosenActivity.resolveInfo.activityInfo.name);

            Iterator iterator = mActivities.keySet().iterator(); // Продвигаемся по всем ключам нашего множества
            Intent tmpIntent = (Intent) iterator.next();

            while (mActivities.get(tmpIntent).size() <= index) { // Пока наш индекс указывает куда-то за массив текущего ключа
                index -= mActivities.get(tmpIntent).size(); // Отнимаем размер массива нашего ключа от индекса
                tmpIntent = (Intent) iterator.next(); // И выбираем следующий ключ, чтобы проделать те же самые действия
            }
            Intent choiceIntent = new Intent(tmpIntent); // Когда мы нашли интент, который нам нужен - 

            // Весь оставшийся код не меняем
        }
    }


ActivityChooserView


Не устали? А ведь ActivityChooserView на пути!

Но всем нам повезло. В нашем искуственном ActivityChooserView нам надо только поменять все ActivityChooserModel на CustomActivityChooserModel. Если учесть, что само ActivityChooserView изменится на CustomActivityChooserView.

Тестирование


Теперь нам надо подготовить данные, которые мы хотим экспортировать:

    private Intent[] getDefaultIntents() {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
        Calendar startCalendar = Calendar.getInstance();
        Calendar endCalendar = Calendar.getInstance();
        try {
            startCalendar.setTime(dateFormat.parse("2015-01-06 00:00:00"));
            endCalendar.setTime(dateFormat.parse("2015-05-06 00:00:00"));
        } catch (ParseException e) {
            e.printStackTrace();
        }

        Intent calendarIntent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI)
                .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startCalendar.getTimeInMillis())
                .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endCalendar.getTimeInMillis())
                .putExtra(CalendarContract.Events.TITLE, "My calendar event")
                .putExtra(CalendarContract.Events.DESCRIPTION, "Group class")
                .putExtra(CalendarContract.Events.EVENT_LOCATION, "Imaginary street 16, Imaginaryland");

        Intent messageIntent = new Intent(Intent.ACTION_SEND);
        messageIntent.putExtra(Intent.EXTRA_TEXT, "Тексту текстово");
        messageIntent.putExtra(Intent.EXTRA_SUBJECT, "Субъекту субъектово");
        messageIntent.setType("text/plain");

        return new Intent[] {calendarIntent, messageIntent};
    }


Мини пример работы:



По такому же принципу можно использовать не только два, но больше интентов для разных типов данных, которыми мы хотим поделиться с приложениями-соседями на нашем или пользовательском устройстве.

Любые правки или предложения принимаются 24/7 в личке или в комментариях (на ваш страх и риск).

На этом всё,
Счастья всем!

— Сноски:
1 — StackOverflow.com
2 — grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android
Tags:
Hubs:
+13
Comments 4
Comments Comments 4

Articles