Brief analysis of Android native Launcher 3

Keywords: Android xml Java Database

Launcher is the first interface to be seen after the android mobile phone is launched, that is, the desktop of the mobile phone system. Let's take the native Launcher 3 of android as an example to see how the interface layout and display data are acquired to briefly analyze the android mobile phone desktop.

The first Activity shown in Launcher is Launcher.java. Let's look at the layout file launcher.xml

//packages/apps/Launcher3/res/layout-land/launcher.xml
<!-- Full screen view projects under the status bar and contains the background -->
<com.android.launcher3.LauncherRootView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res-auto"
    android:id="@+id/launcher"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <com.android.launcher3.dragndrop.DragLayer
        android:id="@+id/drag_layer"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:background="@drawable/workspace_bg"
        android:importantForAccessibility="no"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- The workspace contains 5 screens of cells -->
        <!-- DO NOT CHANGE THE ID -->
        <com.android.launcher3.Workspace
            android:id="@+id/workspace"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            launcher:pageIndicator="@id/page_indicator" />

        <!-- DO NOT CHANGE THE ID -->
        <include layout="@layout/hotseat"
            android:id="@+id/hotseat"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="right"
            launcher:layout_ignoreInsets="true" />

        <include
            android:id="@+id/drop_target_bar"
            layout="@layout/drop_target_bar_vert" />

        <include layout="@layout/overview_panel"
            android:id="@+id/overview_panel"
            android:visibility="gone" />

        <com.android.launcher3.pageindicators.PageIndicatorCaretLandscape
            android:id="@+id/page_indicator"
            android:layout_width="@dimen/dynamic_grid_page_indicator_height"
            android:layout_height="@dimen/dynamic_grid_page_indicator_height"
            android:layout_gravity="bottom|left"/>

        <!-- A place holder view instead of the QSB in transposed layout -->
        <View
            android:layout_width="0dp"
            android:layout_height="10dp"
            android:id="@+id/workspace_blocked_row" />

        <include layout="@layout/widgets_view"
            android:id="@+id/widgets_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="invisible" />

        <include layout="@layout/all_apps"
            android:id="@+id/apps_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="invisible" />

    </com.android.launcher3.dragndrop.DragLayer>

</com.android.launcher3.LauncherRootView>
Then look at the corresponding UI display, you can also look at the layout above.


This interface mainly displays Shortcut and Widget. The Shortcut in the bottom line is special. setIsHotseat attribute is set. When it is displayed, it will be judged whether it is displayed on the bottom line or not. In addition, the number of displays is also default configuration in dw_phone_hotseat.xml. When users want to put other APK shortcuts in this line, it will root. The remaining space after removing the displayed icons determines whether to display the icons separately or in a folder with other apk icons.
All of this data is stored in the database launcher.db.

Hotseat is a subclass of FrameLayout, which retrieves DeviceProfile objects from the getDeviceProfile in Launcher, which contain configuration files for specific devices.

Get all device_profiles in InvariantDeviceProfile.java by getPredefinedDeviceProfiles

//packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
    ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
        ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
        try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
            final int depth = parser.getDepth();
            int type;

            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) {
                    TypedArray a = context.obtainStyledAttributes(
                            Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile);
                    int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0);
                    int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0);
                    float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0);
                    profiles.add(new InvariantDeviceProfile(
                            a.getString(R.styleable.InvariantDeviceProfile_name),
                            a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0),
                            a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0),
                            numRows,
                            numColumns,
                            a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows),
                            a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns),
                            a.getInt(R.styleable.InvariantDeviceProfile_minAllAppsPredictionColumns, numColumns),
                            iconSize,
                            a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0),
                            a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns),
                            a.getFloat(R.styleable.InvariantDeviceProfile_hotseatIconSize, iconSize),
                            a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0)));
                    a.recycle();
                }
            }
        } catch (IOException|XmlPullParserException e) {
            throw new RuntimeException(e);
        }
        return profiles;
    }
The ArrayList < Invariant Device Profile > profiles are obtained by parsing device_profiles.xml reading. FindClosestDevice Profiles and invDistributed Interpolate are finally invoked to obtain the appropriate Invariant Device Profile according to the width and height of the mobile screen. The Invariant Device Profile contains the configuration of the number allowed to be displayed in each row and column.

packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
    ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
            final float width, final float height, ArrayList<InvariantDeviceProfile> points) {

        // Sort the profiles by their closeness to the dimensions
        ArrayList<InvariantDeviceProfile> pointsByNearness = points;
        Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
            public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
                return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                        dist(width, height, b.minWidthDps, b.minHeightDps));
            }
        });

        return pointsByNearness;
    }
Here's a simple configuration of device_profiles.xml

/packages/apps/Launcher3/res/xml/device_profiles.xml
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
    <profile
        launcher:name="Nexus 4"
        launcher:minWidthDps="359"
        launcher:minHeightDps="567"
        launcher:numRows="4"
        launcher:numColumns="4"
        launcher:numFolderRows="4"
        launcher:numFolderColumns="4"
        launcher:minAllAppsPredictionColumns="4"
        launcher:iconSize="60"
        launcher:iconTextSize="13.0"
        launcher:numHotseatIcons="5"
        launcher:hotseatIconSize="56"
        launcher:defaultLayoutId="@xml/default_workspace_4x4"
        />
        ...
</profiles>
default_workspace_4x4.xml

packages/apps/Launcher3/res/xml/default_workspace_4x4.xml
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">

    <!-- Hotseat -->
    <include launcher:workspace="@xml/dw_phone_hotseat" />

    <!-- Bottom row -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
        <favorite launcher:uri="mailto:" />
    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="1"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
        <favorite launcher:uri="#Intent;type=images/*;end" />
    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="3"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
        <favorite launcher:uri="market://details?id=com.android.launcher" />
    </resolve>

</favorites>
dw_phone_hotseat.xml

packages/apps/Launcher3/res/xml/dw_phone_hotseat.xml
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
    <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
    <!-- Dialer, Messaging, [All Apps], Browser, Camera -->
    <resolve
        launcher:container="-101"
        launcher:screen="0"
        launcher:x="0"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
        <favorite launcher:uri="tel:123" />
        <favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="1"
        launcher:x="1"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
        <favorite launcher:uri="sms:" />
        <favorite launcher:uri="smsto:" />
        <favorite launcher:uri="mms:" />
        <favorite launcher:uri="mmsto:" />
    </resolve>

    <!-- All Apps -->

    <resolve
        launcher:container="-101"
        launcher:screen="3"
        launcher:x="3"
        launcher:y="0" >
        <favorite
            launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
        <favorite launcher:uri="http://www.example.com/" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="4"
        launcher:x="4"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
        <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
    </resolve>

</favorites>
Favorites and Workspaces are stored in the favorites and workspace Screens tables in launcher.db, which are only displayed by default. When a user manually puts an application into this block, it will decide whether to display an icon separately or together with another icon in a folder according to the current remaining space. The current situation can be known by default_workspace_4x4.xml. The default configuration on the main screen shows those apks, of course, all of which are saved in the database, so that when the user drags an apk, it will be written to the corresponding database. Here is just the logic to write the default configuration to the database.

//packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
    synchronized private void loadDefaultFavoritesIfNecessary() {
        SharedPreferences sp = Utilities.getPrefs(getContext());

        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
            Log.d(TAG, "loading default workspace");

            AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
            if (loader == null) {
                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
            }
            if (loader == null) {
                final Partner partner = Partner.get(getContext().getPackageManager());
                if (partner != null && partner.hasDefaultLayout()) {
                    final Resources partnerRes = partner.getResources();
                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                            "xml", partner.getPackageName());
                    if (workspaceResId != 0) {
                        loader = new DefaultLayoutParser(getContext(), widgetHost,
                                mOpenHelper, partnerRes, workspaceResId);
                    }
                }
            }

            final boolean usingExternallyProvidedLayout = loader != null;
            if (loader == null) {
                loader = getDefaultLayoutParser(widgetHost);
            }

            // There might be some partially restored DB items, due to buggy restore logic in
            // previous versions of launcher.
            createEmptyDB();
            // Populate favorites table with initial favorites
            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
                    && usingExternallyProvidedLayout) {
                // Unable to load external layout. Cleanup and load the internal layout.
                createEmptyDB();
                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
                        getDefaultLayoutParser(widgetHost));
            }
            clearFlagEmptyDbCreated();
        }
    }
So much, let's first look at the display of the main interface, and then see how the information of all desktop apk s is obtained by calling getActivityList. The first parameter, packageName, is null, querying all the application collection lists configured with intent Action as ACTION_MAIN and Category as CATEGORY_LAUNCHER.


//packages/apps/Launcher3/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
    public List<LauncherActivityInfoCompat> getActivityList(String packageName,
            UserHandleCompat user) {
        final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        mainIntent.setPackage(packageName);
        List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0);
        List<LauncherActivityInfoCompat> list =
                new ArrayList<LauncherActivityInfoCompat>(infos.size());
        for (ResolveInfo info : infos) {
            list.add(new LauncherActivityInfoCompatV16(mContext, info));
        }
        return list;
    }
After entering the main interface is an AllApps ContainerView, inheriting the FrameLayout, the setAdapter set in the constructor is AllAppsGridAdapter AllAppsGridAdapter extends RecyclerView.Adapter, and here calls mAppsRecyclerView.setApps(mApps) to pass in all apps information.

 //packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java
   @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        ...
        // Load the all apps recycler view
        mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
        mAppsRecyclerView.setApps(mApps);
        mAppsRecyclerView.setLayoutManager(mLayoutManager);
        mAppsRecyclerView.setAdapter(mAdapter);
        mAppsRecyclerView.setHasFixedSize(true);
        mAppsRecyclerView.addOnScrollListener(mElevationController);
        mAppsRecyclerView.setElevationController(mElevationController);
     ...
    }
Call onCreateViewHolder and onBindViewHolder in AllAppsGridAdapter to display view

 //packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
  public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case VIEW_TYPE_SECTION_BREAK:
                return new ViewHolder(new View(parent.getContext()));
            case VIEW_TYPE_ICON:
                /* falls through */
            case VIEW_TYPE_PREDICTION_ICON: {
                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                        R.layout.all_apps_icon, parent, false);
                icon.setOnClickListener(mIconClickListener);  //Set the click event listener for each control, and each desktop icon is a custom BubbleTextView
                icon.setOnLongClickListener(mIconLongClickListener);
                icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
                        .getLongPressTimeout());
                icon.setOnFocusChangeListener(mIconFocusListener);

                // Ensure the all apps icon height matches the workspace icons
                DeviceProfile profile = mLauncher.getDeviceProfile();
                Point cellSize = profile.getCellSize();
                GridLayoutManager.LayoutParams lp =
                        (GridLayoutManager.LayoutParams) icon.getLayoutParams();
                lp.height = cellSize.y;
                icon.setLayoutParams(lp);
                return new ViewHolder(icon);
            }
            case VIEW_TYPE_EMPTY_SEARCH:
                return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
                        parent, false));
            case VIEW_TYPE_SEARCH_MARKET:
                View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
                        parent, false);
                searchMarketView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
                    }
                });
                return new ViewHolder(searchMarketView);
            case VIEW_TYPE_SEARCH_DIVIDER:
                return new ViewHolder(mLayoutInflater.inflate(
                        R.layout.all_apps_search_divider, parent, false));
            case VIEW_TYPE_PREDICTION_DIVIDER:
                /* falls through */
            case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
                return new ViewHolder(mLayoutInflater.inflate(
                        R.layout.all_apps_divider, parent, false));
            default:
                throw new RuntimeException("Unexpected view type");
        }
    @Override 
    public void onBindViewHolder(ViewHolder holder, int position) {
        switch (holder.getItemViewType()) {
            case VIEW_TYPE_ICON: {
                AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                BubbleTextView icon = (BubbleTextView) holder.mContent;
                icon.applyFromApplicationInfo(info);
                icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                break;
            }
            case VIEW_TYPE_PREDICTION_ICON: {
                AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                BubbleTextView icon = (BubbleTextView) holder.mContent;
                icon.applyFromApplicationInfo(info);
                icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                break;
            }
            case VIEW_TYPE_EMPTY_SEARCH:
                TextView emptyViewText = (TextView) holder.mContent;
                emptyViewText.setText(mEmptySearchMessage);
                emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
                        Gravity.START | Gravity.CENTER_VERTICAL);
                break;
            case VIEW_TYPE_SEARCH_MARKET:
                TextView searchView = (TextView) holder.mContent;
                if (mMarketSearchIntent != null) {
                    searchView.setVisibility(View.VISIBLE);
                } else {
                    searchView.setVisibility(View.GONE);
                }
                break;
        }
        if (mBindViewCallback != null) {
            mBindViewCallback.onBindView(holder);
        }
    }   
Next, let's look at the construction method of AllAppsGridAdapter, which passes in View.OnClickListener and View.OnLongClickListener

    public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
            iconClickListener, View.OnLongClickListener iconLongClickListener) {
        ...
    }
AllAppsGridAdapter is initialized in the construction method of AllAppsContainerView, as follows:

      public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Resources res = context.getResources();

        mLauncher = Launcher.getLauncher(context);
        mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
        mApps = new AlphabeticalAppsList(context);
        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
        mApps.setAdapter(mAdapter);
        mLayoutManager = mAdapter.getLayoutManager();
        mItemDecoration = mAdapter.getItemDecoration();
        DeviceProfile grid = mLauncher.getDeviceProfile();
        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && !grid.isVerticalBarLayout()) {
            mRecyclerViewBottomPadding = 0;
            setPadding(0, 0, 0, 0);
        } else {
            mRecyclerViewBottomPadding =
                    res.getDimensionPixelSize(R.dimen.all_apps_list_bottom_padding);
        }
        mSearchQueryBuilder = new SpannableStringBuilder();
        Selection.setSelection(mSearchQueryBuilder, 0);
    }
You can see that View.OnClickListener of AllAppsGrid Adapter comes from mLauncher, Launcher.java, and View.OnLongClickListener passes in this, which is handled in this class's onLongClick, so that all item's click events are handled in Launcher's onClick.

//packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
    public void onClick(View v) {
        // Make sure that rogue clicks don't get through while allapps is launching, or after the
        // view has detached (it's possible for this to happen if the view is removed mid touch).
        if (v.getWindowToken() == null) {
            return;
        }

        if (!mWorkspace.isFinishedSwitchingState()) {
            return;
        }

        if (v instanceof Workspace) {
            if (mWorkspace.isInOverviewMode()) {
                showWorkspace(true);
            }
            return;
        }

        if (v instanceof CellLayout) {
            if (mWorkspace.isInOverviewMode()) {
                mWorkspace.snapToPageFromOverView(mWorkspace.indexOfChild(v));
                showWorkspace(true);
            }
            return;
        }

        Object tag = v.getTag();
        if (tag instanceof ShortcutInfo) {
            onClickAppShortcut(v);
        } else if (tag instanceof FolderInfo) {
            if (v instanceof FolderIcon) {
                onClickFolderIcon(v);
            }
        } else if ((FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && v instanceof PageIndicator) ||
                (v == mAllAppsButton && mAllAppsButton != null)) {
            onClickAllAppsButton(v);
        } else if (tag instanceof AppInfo) {//If the apk calls startAppShortcutOrInfoActivity, start the corresponding Activity
            startAppShortcutOrInfoActivity(v);
        } else if (tag instanceof LauncherAppWidgetInfo) {
            if (v instanceof PendingAppWidgetHostView) {
                onClickPendingWidget((PendingAppWidgetHostView) v);
            }
        }
    }

When you click on the icon of each application, you call startAppShortcutOrInfoActivity(View v)

    private void startAppShortcutOrInfoActivity(View v) {
        ItemInfo item = (ItemInfo) v.getTag();
        Intent intent = item.getIntent();
        if (intent == null) {
            throw new IllegalArgumentException("Input must have a valid intent");
        }
        boolean success = startActivitySafely(v, intent, item);
        getUserEventDispatcher().logAppLaunch(v, intent);

        if (success && v instanceof BubbleTextView) {
            mWaitingForResume = (BubbleTextView) v;
            mWaitingForResume.setStayPressed(true);
        }
    }
In this method, the startActivitySafely of this class is then called, and eventually in this Launcher AppsCompat. getInstance (this). startActivityForProfile

       public void startActivityForProfile(ComponentName component, UserHandleCompat user,
            Rect sourceBounds, Bundle opts) {
        Intent launchIntent = new Intent(Intent.ACTION_MAIN);
        launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        launchIntent.setComponent(component);
        launchIntent.setSourceBounds(sourceBounds);
        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(launchIntent, opts);
    }
In this way, every click on an icon will start a corresponding Activity. At first, I just want to see the process of clicking on apk to start the application. I found the click event of apk icon for half a day. In this incidental record, of course, there are many desktop layout information from the database, update layout information and so on. It is not mentioned that the analysis is mainly based on the acquisition of apk information and icon click event. Yes, when there are multiple applications in the phone with the following two attributes, you will see that the user will choose which desktop to use.

<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
Here's a brief summary of the desktop display process
1. After the mobile phone starts, it will start the desktop first. In Launcher's database, there will be detailed information of each apk's display location, specifically in the way of coordinates.
2. Query all apk information that needs to be displayed on the desktop through getActivityList and return a collection.
3. The main interface is a custom AllApps Recycler View inheriting Recycler View. Setting up AllApps Grid Adapter, how many BubbleTextView s will be created according to the size of the list set above (that is, the desktop icon you see).

Posted by benutne on Fri, 19 Apr 2019 14:36:34 -0700