[Из песочницы] Как я одной кнопкой шарил разные данные в Android приложении

d6a9dd15628c4050ac6b560a7d278e0b.jpgКак-то раз передо мной встала задача добавить экспорт в календарь к уже написанному экспорту обычных текстовых данных через 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 mActivities = new ArrayList();

/* … */ // — это не смайлик

private boolean loadActivitiesIfNeeded () { if (mReloadActivities && mIntent!= null) { mReloadActivities = false; mActivities.clear (); List 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> mActivities = new LinkedHashMap>(); // Во-первых, понимаем, что объект mActivities нам следует изменить, чтобы знать, к какому интенту относится та или иная активити (ту мэни инглиш вордс. неверзелесс, продолжаем-с)

/* … */

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

for (Intent intent: mIntents) { // Добавляем цикл по массиву List resolveInfos = mContext.getPackageManager ().queryIntentActivities (intent, 0); ArrayList 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> activitiesValues = mActivities.values ();

ArrayList activitiesList = new ArrayList<>();

for (ArrayList 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 arrayList: mActivities.values ()) { // Входим в цикл if (! arrayList.isEmpty ()) { return arrayList.get (0).resolveInfo; // Если массив не пустой — возвращаем ResolveInfo первого элемента } } } } return null; // Ну и никогда не лишним вернуть null } Остаются два метода: getActivityIndex () и chooseActivity ().

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

List activities = mActivities; final int activityCount = activities.size (); И расписать всё то же, только с несколькими ArrayList, которые мы держим в mActivities:

HashMap> activities = mActivities;

Collection> activitiesValues = activities.values (); ArrayList activitiesList = new ArrayList<>(); for (ArrayList 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}; } Мини пример работы: b223f6ff26db4a7e86cc280486fe4dfc.jpgc59d32ee9c7b4d649c70fda99d7fd0a4.jpg

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

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

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

— Сноски:1 — StackOverflow.com2 — grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/

© Habrahabr.ru