Ming Xiu's "stack" way - overcoming the Android startup stack trap

Author: vivo Internet front-end team - Zhao Kaiping

This article starts from a problem encountered in a business case, and uses the flag FLAG_ACTIVITY_NEW_TASK as an entry point to take you to explore an important task before the Activity starts-stack verification.

The article lists a series of abnormal conditions that may be encountered in the business, describes in detail the "pit" that may be encountered when using FLAG_ACTIVITY_NEW_TASK, and explores its root cause from the source code. Only by using flag and launchMode reasonably can we avoid a series of unexpected startup problems due to the particularity of the stack mechanism.

1. Problem and Background

Interconnection and mutual jump between applications is an important means to achieve system integrity and experience consistency, and it is also the simplest method.

When we use the most common method to startActivity, we will encounter failure. In real business, we encountered such an exception: when a user clicks a button, he wants to "simplely" jump to another application, but nothing happens.

With rich experience, do you have various conjectures in your mind: Is it because the target Activity or even the target App does not exist? Is the target Activty not open to the outside world? Is there a permission restriction or the action/uri of the jump is wrong...

The real reason is hidden by layers of features such as flag, launchMode, and Intent, which may exceed your thinking at this time.

This article will start from the source code, explore the cause and effect, expand and talk about the "tribulations" that need to go through before startActivity() is really ready to start an Activity, and how to solve the startup exception caused by the stack problem according to the evidence.

1.1 Problems encountered in business

The scenario in the business is like this, there are three applications A, B, and C.

(1) Jump from application A-Activity1 to application B-Activity2;

(2) Application B-Activity2 continues to jump to application C-Activity3;

(3) A button in C will jump to B-Activity2 again, but nothing happens after clicking. If you don't go through the previous jump from A to B, it is possible for C to jump directly to B.

1.2 Question Code

The Androidmanifest configurations of the three activities are as follows, all of which can be launched by their respective actions, and the launchMode is the standard mode.

<!--应用A--> 
      <activity
            android:name=".Activity1"
            android:exported="true">
            <intent-filter>
                <action android:name="com.zkp.task.ACTION_TO_A_PAGE1" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
 
<!--应用B-->
        <activity
            android:name=".Activity2"
            android:exported="true">
            <intent-filter>
                <action android:name="com.zkp.task.ACTION_TO_B_PAGE2" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
 
<!--应用C-->
        <activity
            android:name=".Activity3"
            android:exported="true">
            <intent-filter>
                <action android:name="com.zkp.task.ACTION_TO_C_PAGE3" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Codes from A-1 to B-2, specify flag as FLAG_ACTIVITY_NEW_TASK

private void jumpTo_B_Activity2_ByAction_NewTask() {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
}

Codes from B-2 to C-3, no flag specified

private void jumpTo_C_Activity3_ByAction_NoTask() {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3");
    startActivity(intent);
}

The codes from C-3 to B-2 are exactly the same as those from A-1 to B-2, and the specified flag is FLAG_ACTIVITY_NEW_TASK

private void jumpTo_B_Activity2_ByAction_NewTask() {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
}

1.3 Preliminary analysis of the code

Looking at the problem code carefully, it is very simple in implementation and has two characteristics:

(1) If you jump directly to B-2 through C-3, there is no problem, but after A-1 has skipped B-2, C-3 fails.

(2) When A-1 and C-3 jump to B-2, the flag is set to FLAG_ACTIVITY_NEW_TASK.

Based on experience, we speculate that it is related to the stack, and try to print out the status of the stack before the jump, as shown in the figure below.

Since FLAG_ACTIVITY_NEW_TASK is set when A-1 jumps to B-2, and it is not set when B-2 jumps to C-3, 1 is in an independent stack, and 2 and 3 are in another stack. As shown in the figure below.

C-3 generally has three possible expectations for jumping to B-2, as shown in the figure below: Envision 1, create a new Task, and start a B-2 in the new Task; Envision 2, reuse the existing B-2; Envision 3 , Create a new instance B-2 in the existing Task.

But in fact, none of the three expectations are realized, and any life cycle of all activities remains unchanged, and the interface always stays at C-3.

Take a look at the official comments and code comments of FLAG_ACTIVITY_NEW_TASK, as shown below:

Focus on this paragraph:

When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in.

When using this flag, if the Activity you are starting is already running in a Task, then a new Activity will not be started; instead, the current Task will simply be displayed in front of the interface, showing its last state.

——Obviously, the official documents and code comments are consistent with our abnormal phenomenon. If the target Activity2 already exists in the Task, it will not be started; the Task will be displayed directly in front and show the final state. Since the target Activty3 is the source Activity3, there is no change on the page.

It seems that the official is still very reliable, but can the actual effect really always be consistent with the official description? Let's look at a few scenarios.

2. Scenario expansion and verification

2.1 Scene Expansion

In the process of adjusting and reproducing according to the official description, the author found several interesting scenes.

PS: In the business case above, B-2 and C-3 are in different applications and in the same task, but whether they are actually the same application has little impact on the result. In order to avoid reading confusion caused by different applications and different tasks, we all perform jumps in the same stack in this application, so the scene in the business is equivalent to the following [Scenario 0]

[Scenario 0] Change the inter-application jump from B-2 to C-3 in the business to the intra-application jump from B-2 to B-3

// B-2跳转B-3
public static void jumpTo_B_3_ByAction_Null(Context context) {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");
    context.startActivity(intent);
}

As shown in the figure below, A-1 sets NEW_TASK to jump to B-2, then jumps to B-3, and finally sets NEW_TASK to jump to B-2. Although jumping C-3 was changed to jumping B-3, it was consistent with the performance of the previous question, there was no response, and it stayed at B-3.

Some readers will point out such a problem: If you use NEW_TASK to jump in the same application without specifying the taskAffinity attribute of the target, it is actually impossible to start in the new Task. Please ignore this problem. It can be considered that the author's operation has added taskAffinity, which has no effect on the final result.

[Scenario 1] If the target task and the source task are not the same, will the existing task be reused and the latest status displayed as stated in the official document? We change to B-3 to start a new Activity C-4 of a new Task, and then jump back to B-2 through C-4

// B-3跳转C-4
public static void jumpTo_C_4_ByAction_New(Context context) {
    Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4");
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}
// C-4跳转B-2
public static void jumpTo_B_2_ByAction_New(Context context) {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}

As shown in the figure below, A-1 sets NEW_TASK to jump to B-2, then jumps to B-3, then sets NEW_TASK to jump to C-4, and finally sets NEW_TASK to jump to B-2.

The expected result is: instead of jumping to B-2, it jumps to B-3 at the top level of the Task where it is located.

The actual result: as expected, a jump to B-3 was indeed made.

[Scene 2] Slightly modify scene 1: when C-4 to B-2, we do not jump through action, but jump through setClassName instead .

// C-4跳转B-2
public static void jumpTo_B_2_ByPath_New(Context context) {
    Intent intent = new Intent();
    intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 直接设置classname,不通过action
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}

As shown in the figure below, A-1 sets NEW_TASK to jump to B-2, then jumps to B-3, then sets NEW_TASK to jump to C-4, and finally sets NEW_TASK to jump to B-2.

The expected result is: consistent with scene 0, it will jump to the existing top-level B-3 of the Task where B-2 is located.

The actual result is: in the existing Task2, a new B-2 instance is generated.

Just changing the way to re-jump B-2, the effect is completely different! This is inconsistent with the behavior of the flag and the "singleTask" launchMode value mentioned in the official document!

[Scenario 3] Modify scene 1 again: this time C-4 does not jump to B-2 at the bottom of the stack, but instead jumps to B-3 , and it still uses action.

// C-4跳转B-3
public static void jumpTo_B_3_ByAction_New(Context context) {
    Intent intent = new Intent();
    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}

As shown in the figure below, A-1 sets NEW_TASK to jump to B-2, then jumps to B-3, then sets NEW_TASK to jump to C-4, and finally sets NEW_TASK to jump to B-3.

The expected result is: consistent with scene 0, it will jump to B-3 at the top level of the Task where B-2 is located.

The actual result is: in the existing Task2, a new instance of B-3 is created.

Isn't it good to say that when the Activity already exists, show the latest status of the Task it is in? Obviously, there is already B-3 in Task2, but it is not displayed directly, but a new instance of B-3 is generated.

[Scenario 4] Since the Activity is not reused, will the Task be reused? Slightly modify scene 3 and directly assign a separate affinity to B-3.

<activity
    android:name=".Activity3"
    android:exported="true"
    android:taskAffinity="b3.task"><!--指定了亲和性标识-->
    <intent-filter>
        <action android:name="com.zkp.task.ACTION_TO_B_PAGE3" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

As shown in the figure below, A-1 sets NEW_TASK to jump to B-2, then jumps to B-3, then sets NEW_TASK to jump to C-4, and finally sets NEW_TASK to jump to B-3.

——This time, even Task will not be reused anymore... Activity3 is instantiated in a new stack.

Looking back at the official notes, it will appear very inaccurate, and even cause serious errors in the developer's perception of this part! Slightly changing an unrelated attribute in the process (such as jump target, jump method...) can make a big difference.

When looking at flag-related comments, we must establish an awareness: the actual effect of Task and Activity jumps is the result of the combined effects of attributes such as launchMode, taskAffinity, jump mode, and the level of Activity in Task. Don't believe "one-sided words."

Back to the question itself, what are the reasons for the above different effects? Only the source code is the most trustworthy.

3. Scenario analysis and source code exploration

This article is based on the Android 12.0 source code to explore. The performance of the above scenarios on different Android versions is consistent.

3.1 Notes on source code debugging

The debugging method of the source code has been taught in detail in many articles, so this article will not repeat them. Here is just a brief summary of the things that need attention :

  1. When downloading the emulator, do not use the Google Play version. This version is similar to the user version and cannot select the system_process process for breakpoints.

  2. Even for Google's official simulator and source code, there will be cases where the number of lines seriously does not correspond to the breakpoint (for example: the simulator will actually run to method A, but when the breakpoint is broken in the source code, the actual method cannot be located The number of lines corresponding to A), there is no good solution to this problem, and we can only avoid it as much as possible, such as keeping the simulator version consistent with the source code version, setting more breakpoints to increase the probability of key lines being located.

3.2 Preliminary breakpoint, clear start result

Taking [Scenario 0] as an example, let’s preliminarily confirm why there is no response when B-3 jumps to B-2, and whether the system has notified the reason.

3.2.1 Clarify the start-up results and their sources

In the breakpoint debugging of Android source code, there are two common types of processes: application process and system_process process.

In the application process, we can get the status code result of the application startup result, which is used to tell us whether the startup is successful. The involved stack is shown in the following figure (marker 1):

Activity class:: startActivity() → startActivityForResult() →  Instrumentation class:: execStartActivity(), the return value result is the result of ATMS (ActivityTaskManagerService) execution.

As marked in the figure above (Mark 2), the ATMS class:: startActivity() method returns result=3.

In the system_process process, let's see how the result=3 is assigned. The detailed breakpoint steps are omitted, and the actual stack is shown in the figure below (mark 1):

ATMS class:: startActivity() → startActivityAsUser() →  ActivityStarter class:: execute() → executeRequest() → startActivityUnchecked() → startActivityInner() → recycleTask(), the result is returned in recycleTask().

As shown in the figure above (note 2), the result is assigned when mMovedToFront=false, that is, result=START_DELIVERED_TO_TOP=3, and START_SUCCESS=0 means the creation is successful.

Take a look at the description of START_DELIVERED_TO_TOP in the source code, as shown below:

Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity.

(Result of IActivityManaqer.startActivityActivity: The Activity is not actually started, but the given Intent was given to an existing top-level Activity.)

"Activity is not really started" - yes, because it can be reused

"The given Intent has been provided to the existing top-level Activity" - actually no, the top-level Activity3 did not receive any callbacks, onNewIntent() was not executed, and even tried to pass in new parameters through Intent:: putExtra(), Activity3 Also not received. The official document brings us another question point? We record this problem and analyze it later.

What conditions must be met to cause the result of START_DELIVERED_TO_TOP? The author's idea is to find out the differences by comparing with the normal startup process.

3.3 Process breakpoint, explore the startup process

Generally speaking, when locating a problem, we are used to inferring the cause through the result, but the process of inversion can only focus on the code branch that is strongly related to the problem, and cannot give us a good understanding of the whole picture.

Therefore, in this section, we will introduce the logic that is strongly related to the above [scenario 01234] in the process of startActivity through sequential reading. Briefly again:

  1. [Scene 0] In the same Task, jump from top B-3 to B-2—stay at B-3

  2. [Scenario 1] From C-4 in another Task, jump to B-2——jump to B-3

  3. [Scenario 2] Change the method of jumping from C-4 to B-2 in Scene 1 to setClassName()——create a new B-2 instance

  4. [Scenario 3] In scenario 1, change the jump from C-4 to B-2 to jump to B-3 - create a new instance of B-3

  5. [Scenario 4] Assign taskAffinity to B-3 in Scenario 3 - Create a new Task and a new B-3 instance

3.3.1 Overview of process source code

In the source code, the entire startup process is very long, and there are many methods and logics involved. In order to make it easier for everyone to sort out the order of method calls and facilitate the reading of subsequent content, the author organizes the key classes and method call relationships involved in this article as follows.

If you don’t know the calling relationship in the follow-up reading, you can go back here to check:

// ActivityStarter.java
 
    ActivityStarter::execute() {
        executeRequest(intent) {
            startActivityUnchecked() {
                startActivityInner();
        }
    }
    ActivityStarter::startActivityInner() {
        setInitialState();
        computeLaunchingTaskFlags();
        Task targetTask = getReusableTask(){
            findTask();
        }
        ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity();
        if (targetTaskTop != null) {
            startResult = recycleTask() {
                setTargetRootTaskIfNeeded();
                complyActivityFlags();
                if (mAddingToTask) {
                    return START_SUCCESS; //【场景2】【场景3】从recycleTask()返回
                }
                resumeFocusedTasksTopActivities()
                return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【场景1】【场景0】从recycleTask()返回
            }
        } else {
            mAddingToTask = true;
        }
        if (startResult != START_SUCCESS) {
            return startResult;//【场景1】【场景0】从startActivityInner()返回
        }
        deliverToCurrentTopIfNeeded();
        resumeFocusedTasksTopActivities();
        return startResult;
    }

3.3.2 Key process analysis

(1) Initialization

startActivityInner() is the most important method. As shown in the following figures, this method will first call setInitialState() to initialize various global variables, and call reset() to reset various states in ActivityStarter.

Along the way, we make a note of two key variables, mMovedToFront and mAddingToTask, which are both reset to false here.

Among them, mMovedToFront represents whether the target Task needs to be moved to the foreground when the Task is reusable; mAddingToTask represents whether to add the Activity to the Task.

(2) Calculate and confirm the flag when starting

In this step, the computeLaunchingTaskFlags() method will be used to perform preliminary calculations based on the launchMode and the attributes of the source Activity to confirm the LaunchFlags.

Here we focus on dealing with various scenarios where the source Activity is empty, which has nothing to do with the scenarios we mentioned above, so we won’t explain them any further.

(3) Obtain a task that can be reused

This step is implemented by calling getReusableTask() to find out whether there is a task that can be reused.

Let me talk about the conclusion first: In scene 0123, reusable tasks can be obtained, but in scene 4, reusable tasks are not obtained.

Why can't scene 4 be reused? Let's look at the key implementation of getReusableTask().

In the figure above (note 1), putIntoExistingTask represents whether an existing Task can be put in. When the flag contains NEW_TASK and does not contain MULTIPLE_TASK, or the launchMode of singleInstance or singleTask is specified, and no Task is specified or the result is required to be returned, the conditions of scenario 01234 are met.

Then, in the above figure (label 2), find the task that can be reused through findTask(), and assign the activity at the top of the stack found in the process to the intentActivity. Finally, the above figure (label 3) takes the Task corresponding to the intentActivity as the result.

How does findTask() find which Task can be reused?

It is mainly to confirm the two results mIdealRecord - "ideal ActivityRecord" and mCandidateRecord - "candidate ActivityRecord", as the intentActivity, and take the Task corresponding to the intentActivity as the multiplexing Task.

What ActivityRecord is the ideal or candidate ActivityRecord? Confirmed in mTmpFindTaskResult.process().

The program will traverse all the Tasks in the current system, and in each Task, perform the work shown in the figure above - compare the bottom Activity realActivity of the Task with the target Activity cls.

In scene 012, we want to jump to Activity2, that is, cls is Activity2, which is the same as realActivity2 at the bottom of the Task, so Activity3 r at the top of the Task is taken as the "ideal Activity";

In scenario 3, we want to jump to Activity3, that is, cls is Activity3, which is different from realActivity2 at the bottom of the Task. Then we further judge that the stack affinity rows of Activity2 at the bottom of the task and the target Activity3 have the same affinity, and use the top Activity3 of the Task as "Candidate Activity";

In Scenario 4, all conditions are not met, and no reusable Task can be found in the end. Assign mAddingToTask to true after executing getReusableTask()

From this, we can explain the phenomenon of creating a new Task in [Scenario 4].

(4) Determine whether the target Task needs to be moved to the foreground

If there is a reusable Task, scene 0123 will execute recycleTask(), and several operations will be performed successively in this method: setTargetRootTaskIfNeeded(), complyActivityFlags().

First, the program will execute setTargetRootTaskIfNeeded() to determine whether the target Task needs to be moved to the foreground, using mMovedToFront as the identifier.

In [Scene 123], the source Task and the target Task are different, differentTopTask is true, and after a series of Task attribute comparisons, it can be concluded that mMovedToFront is true;

In scene 0, the source Task and the target Task are the same, differentTopTask is false, and mMovedToFront remains initially false.

From this, we can explain that in [Scene 0], the Task does not switch.

(5) Confirm whether to add Activity to Task by comparing flag, Intent, Component, etc.

Still in [Scene 0123], recycleTask() will continue to execute complyActivityFlags() to confirm whether to add the Activity to the Task, using mAddingToTask as the identifier.

This method will make a series of judgments on many flag and Intent information such as FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_CLEAR_TASK, FLAG_ACTIVITY_CLEAR_TOP, etc.

In the above figure (marked 1), it will first judge whether to reset the Task, resetTask, and the judgment condition is FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. Obviously, the resetTask of scene 0123 is false. Continue to execute.

Then, there will be a variety of conditional judgments executed in sequence.

In [Scene 3], the target Component (mActivityComponent) is B-3, and the realActivity of the target Task is B-2. The two are different, and the judgment related to resetTask is entered (mark 2).

ResetTask was already false before, so mAddingToTask in [Scenario 3] is set to true out of the original value.

In [Scene 012], the two activities compared are both B-2 (marked 3), and you can enter the next level of judgment - isSameIntentFilter().

The content of the judgment in this step is obvious. The existing Intent of the target Activity2 is compared with the new Intent. Obviously, in Scenario 2, the Intent is naturally different due to the change to setClassName jump.

Therefore, mAddingToTask in [Scenario 2] deviates from the original value and is set to true.

Take a look at the summary:

[Scene 123] mMovedToFront is first set to true, and [Scene 0] has gone through many tests, keeping the initial value as false.

——This means that when there are reusable tasks, [scene 0] does not need to switch the task to the front; [scene 123] needs to switch to the target task.

[Scene 234] mAddingToTask is set to true at different stages, while in [Scene 01], the initial value is always false.

——This means that [Scene 234] needs to add the Activity to the Task, but [Scene 01] no longer needs it.

(6) Actually start the Activity or directly return the result

Each activity that is started will start the real startup and life cycle calls through a series of operations such as resumeFocusedTasksTopActivities().

Our exploration of the above-mentioned scenarios has already got the answer, and the follow-up process will no longer be concerned.

4. Problem repair and remaining problem solving

4.1 Bug fixes

Since so many necessary conditions have been summarized above, we only need to destroy some of them to repair the problems encountered in the business, and briefly list a few solutions.

  • Solution 1: Modify the flag. When jumping from B-3 to B-2, add FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_CLEAR_TOP, or don’t set the flag directly. Proven to be feasible.

  • Solution 2: Modify the intent attribute, that is, [Scene 2]. A-1 implicitly jumps to B-2 through action, then B-3 can jump to B-2 through setClassName or modifying the attributes in the action. Proven to be feasible.

  • Option 3: Remove B-2 early. When B-2 jumps to B-3, finish drops B-2. It should be noted that finish() should be executed before startActivity() to avoid the influence of the remaining ActivityRecord and Intent information on subsequent jumps. Especially when you use B-2 as the deep link of your own application to distribute the activity, it is more worthy of vigilance.

4.2 Remaining issues

Remember some of our doubts at the beginning of the article, why there is no callback onNewIntent()?

onNewIntent() will be triggered by deliverNewIntent(), and deliverNewIntent() is only called by the following two methods.

complyActivityFlags() is the method we focused on in 3.3.1.5 above. It can be found that all conditions in complyActivityFlags() that may call deliverNewIntent() are perfectly avoided.

The deliverToCurrentTopIfNeeded() method is shown in the figure below.

mLaunchFlags and mLaunchMode cannot meet the conditions, resulting in dontStart being false and missing deliverNewIntent().

So far, the question of onNewIntent() has been answered.

V. Conclusion

Through a series of scenario assumptions, we found many unexpected phenomena:

  1. The document mentions that FLAG_ACTIVITY_NEW_TASK is equivalent to singleTask, which is not exactly the case. Only when combined with other flags can a similar effect be achieved. The annotation of this flag is very one-sided and may even lead to misunderstandings. A single factor cannot determine the overall performance.

  2. The official document mentions that START_DELIVERED_TO_TOP will pass the new Intent to the top-level Activity, but in fact, not every START_DELIVERED_TO_TOP will redistribute the new Intent.

  3. For the same activity at the bottom of the stack, when jumping to it through action or setClassName twice, the second jump will fail, but when the two jumps are made in different ways, it will succeed.

  4. When simply using FLAG_ACTIVITY_NEW_TASK, the effect of jumping to the bottom of the stack is quite different from jumping to other activities in the same stack.

The problems encountered in the business, in the final analysis, are caused by insufficient understanding of the Android stack mechanism.

In the face of stack-related coding, developers must think clearly about the mission of Activty, which undertakes the newly opened application stack, in the overall application. It is necessary to comprehensively evaluate Task history, flag attributes, launchMode attributes, Intent content, etc., and refer to them carefully. Only official documents can avoid stack traps and achieve ideal and reliable results.

Guess you like

Origin blog.csdn.net/vivo_tech/article/details/130199165
Recommended