📜 ⬆️ ⬇️

How I fumbled with one button different data in the Android application



Once I faced the task of adding export to the calendar to the already written export of plain text data via the ShareActionProvider button. Immediately there were several options, each of which for some reason did not suit me.

SO 1 suggested that I change the MIME type from “text / plain” to “* / *” to cover a larger number of installed applications. This added a lot of unnecessary applications, and the necessary ones were lost in the sea of ​​unnecessary ones. There were proposals to use libraries, also, SO proposed to create your own Intent Chooser, and in it to implement the logic of choice, what data should be exported. I did not want to use the dialog box only in order to be able to choose from several types of applications - and I decided to deal with the ShareActionProvider source code.

Digging in the source:


First of all, my view fell on the setShareIntent method, which took the collected Intent with the data for export. And what if you can make a universal intent, I asked myself and rushed to look for how to combine the two intents into one, and even with different actions (Intent.ACTION_INSERT and Intent.ACTION_SEND). Not a single solution that I found (I didn’t dig too deeply, to be honest), so I decided to peek at what is being done under the hood of the ShareActionProvider class. Looking ahead, I’m saying that getting source 2 from Google, finding classes that work with our intent, and repeating steps 1 and 2 several times, I found out that there are three classes in charge of everything: Actually, ShareActionProvider, ActivityChooserView and ActivityChooserModel. The last two are responsible for selecting the necessary applications for our intention, creating a drop-down list and processing the list selection.
')
The solution to the problem I decided to start with changing the type of data that I will pass to setShareIntent (). Logically, if I want to export more different data, I need more intents and, therefore, the first solution that comes to mind is to use an array:

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); } 


change to:

  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 } 


The first step is passed, the first method is changed, we go further along the chain. The following problem manifested itself in the dataModel object. He (or she, model) does not want to take our array. What to do, go inside ActivityChooserModel.get () and see what we can change there:

  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; } } 


In fact, in this method, we changed only the class name from ActivityChooserModel to ours. From here, our path goes through sDataModelRegistry to the get () method, but sDataModelRegistry is just the Map set, which is returned to us by an object of type ActivityChooserModel. Vicious circle. We leave the mental cycle and try another approach -> if the dataModel is an object of type ActivityChooserModel, it means that it has a setIntent () method. It remains for us (too naively) to only change the type of its input parameter to an array:

  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 


We continue the excavation. Add one more method to ensureConsistentState () in our carbon form stack, and dive into it with a head for editing and find two methods - loadActivitiesIfNeeded () and sortActivitiesIfNeeded (). These are the ones we need to fix. We mentally hope that the trend will not continue, and we will not end with sixteen methods in the fifth step.

We start with the first method:

  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; } 


change to:

  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; } 


We are moving to the sorting method, everything is simple, we add a loop through the array instead of a single element. Now we know that everything is stored in the set, therefore no input parameters of the method are required:

  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; } 


We clean the code behind us


Looking around. We also have methods that disagree with our changes: getActivity (), getActivityIndex (), the same chooseActivity (), already with a new error, then getDefaultActivity () and setDefaultActivity (). Looking closer - we see that they swear only for changes in the type of mActivities from ArrayList to LinkedHashMap, then:

Add a method to get the ActivityResolveInfo by index

  /** * 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); } } 


This method will help us more. After that we change:

  public ResolveInfo getActivity(int index) { synchronized (mInstanceLock) { ensureConsistentState(); return mActivities.get(index).resolveInfo; } } 


On:

  public ResolveInfo getActivity(int index) { return getActivityResolveInfo(index).resolveInfo; } 


It's simple ...

We recall what has been done and what remains:


Let's do the default activation. You need to adapt them to use the Map:

In the setDefaultActivity () method, we only take an ArrayList by the first key:
  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); //    


As for getDefaultActivity ():

  public ResolveInfo getDefaultActivity() { synchronized (mInstanceLock) { ensureConsistentState(); if (!mActivities.isEmpty()) { return mActivities.get(0).resolveInfo; } } return null; } 


We need to get the first element of the first key:

  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 } 


Two methods remain: getActivityIndex () and chooseActivity ().

To get the activation index, we need to take the string

  List<ActivityResolveInfo> activities = mActivities; final int activityCount = activities.size(); 


And paint all the same, only with a few ArrayList, which we keep in 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(); 


Now we need to choose aktiviti, a lot of changes, so I will give the whole method, sorry for a bunch of code :-(

Old method:
  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 method:
  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


Not tired? But ActivityChooserView is on the way!

But we were all lucky. In our artificial ActivityChooserView, we just need to change all ActivityChooserModel to CustomActivityChooserModel. If we consider that the ActivityChooserView itself will change to CustomActivityChooserView.

Testing


Now we need to prepare the data we want to export:

  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}; } 


Mini example of work:



By the same principle, you can use not only two, but more intents for different types of data that we want to share with neighboring applications on our or user device.

Any edits or suggestions are accepted 24/7 in a personal or in the comments (at your own risk).

That's all,
Happiness to all!

- Footnotes:
1 - StackOverflow.com
2 - grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android

Source: https://habr.com/ru/post/255133/


All Articles