Как я одной кнопкой шарил разные данные в 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<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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *