[Из песочницы] Как я одной кнопкой шарил разные данные в Android приложении
Как-то раз передо мной встала задача добавить экспорт в календарь к уже написанному экспорту обычных текстовых данных через 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
/* … */ // — это не смайлик
private boolean loadActivitiesIfNeeded () {
if (mReloadActivities && mIntent!= null) {
mReloadActivities = false;
mActivities.clear ();
List
private final LinkedHashMap
/* … */
private boolean loadActivitiesIfNeeded () { if (mReloadActivities && mIntents!= null) { mReloadActivities = false; mActivities.clear ();
for (Intent intent: mIntents) { // Добавляем цикл по массиву
List
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
for (ArrayList
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
Чтобы получить индекс активити — нам надо взять строку
List
HashMap
Collection
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.com2 — grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/