Как-то раз передо мной встала задача добавить экспорт в календарь к уже написанному экспорту обычных текстовых данных через 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/
Автор: NonGrate