Как-то раз передо мной встала задача добавить экспорт в календарь к уже написанному экспорту обычных текстовых данных через 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/
ссылка на оригинал статьи http://habrahabr.ru/post/255133/
Добавить комментарий