= _text
13 | }
14 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/ReserveConfig.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | /**
7 | * Package: osp.leobert.android.tracker.pager
8 | * Classname: ReserveConfig
9 | * Created by leobert on 2020/5/14.
10 | */
11 | @Retention(RetentionPolicy.RUNTIME)
12 | public @interface ReserveConfig {
13 | String on();
14 |
15 | String asPoint();
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notifications_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/ITrackedPager.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | /**
6 | * Package: osp.leobert.android.tracker.pager
7 | * Classname: ITrackedPager
8 | * Created by leobert on 2020/5/14.
9 | */
10 | public interface ITrackedPager {
11 | void setPagerToken(@NonNull String pagerToken);
12 |
13 | @NonNull
14 | String getPagerToken();
15 |
16 | interface FragmentInViewPager extends ITrackedPager{
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/TrackerBpContext.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Package: osp.leobert.android.tracker.pager
10 | * Classname: TrackerBpContext
11 | * Created by leobert on 2020/5/19.
12 | */
13 | @Retention(RetentionPolicy.RUNTIME)
14 | @Target(ElementType.METHOD)
15 | public @interface TrackerBpContext {
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/FragmentFactoryWrapper.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentFactory
5 |
6 | /**
7 | * Package: osp.leobert.android.tracker.pager
8 | * Classname: FragmentFactoryWrapper
9 | * Created by leobert on 2020/5/19.
10 | */
11 | class FragmentFactoryWrapper(val wrapper: FragmentFactory) : FragmentFactory() {
12 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
13 | return wrapper.instantiate(classLoader, className).apply {
14 | this.lifecycle.addObserver(FragmentTrackerImpl())
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import osp.leobert.android.tracker.pager.ITrackedPager
5 |
6 | /**
7 | * Package: osp.leobert.android.pagertrackerdemo
8 | * Project: PagerTrackerDemo
9 | * Classname: BaseActivity
10 | * Created by leobert on 2020/6/30.
11 | */
12 | abstract class BaseActivity : AppCompatActivity(), ITrackedPager {
13 | private var mPagerToken: String? = null
14 |
15 | override fun setPagerToken(pagerToken: String) {
16 | mPagerToken = pagerToken
17 | }
18 |
19 | override fun getPagerToken(): String = mPagerToken ?: ""
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo_notify.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/index/ui/notifications/NotificationsViewModel.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.index.ui.notifications
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 |
7 | class NotificationsViewModel : ViewModel() {
8 |
9 | private val _notifications = MutableLiveData>().apply {
10 | value = arrayListOf(
11 | Notification("李白", "李白的id", "准备点菜,我马上找你来喝酒"),
12 | Notification("杜甫", "杜甫的id", "我和李白一起来"),
13 | Notification("李白","李白的id","多准备点,小杜想要一起来")
14 | )
15 | }
16 |
17 | val notifications: LiveData> = _notifications
18 |
19 | }
20 |
21 | data class Notification(val userName: String, val userId: String, val message: String)
--------------------------------------------------------------------------------
/tracker/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/tracker/src/androidTest/java/osp/leobert/android/tracker/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("osp.leobert.android.tracker.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/osp/leobert/android/pagertrackerdemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("osp.leobert.android.pagertrackerdemo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/bio/BioActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.bio
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import osp.leobert.android.pagertrackerdemo.BaseActivity
7 | import osp.leobert.android.pagertrackerdemo.R
8 | import osp.leobert.android.pagertrackerdemo.launchActivity
9 | import osp.leobert.android.tracker.pager.TrackedPager
10 |
11 | @TrackedPager(pagerPoint = "P_BIO")
12 | class BioActivity : BaseActivity() {
13 | companion object {
14 | const val EXT_STR_ID = "EXT_STR_ID"
15 | fun launch(context: Context, userId: String) {
16 | Intent(context, BioActivity::class.java)
17 | .putExtra(EXT_STR_ID, userId)
18 | .launchActivity(context)
19 | }
20 |
21 | }
22 |
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(R.layout.activity_bio)
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_bio.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
15 |
16 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 leobert
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tracker/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | android {
5 | compileSdkVersion 29
6 | buildToolsVersion "29.0.2"
7 |
8 |
9 | defaultConfig {
10 | minSdkVersion 15
11 | targetSdkVersion 29
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles 'consumer-rules.pro'
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
31 | implementation 'androidx.appcompat:appcompat:1.1.0'
32 | implementation 'androidx.core:core-ktx:1.1.0'
33 | testImplementation 'junit:junit:4.12'
34 | androidTestImplementation 'androidx.test:runner:1.1.1'
35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo
2 |
3 | import android.os.Bundle
4 | import android.widget.TextView
5 | import androidx.lifecycle.lifecycleScope
6 | import kotlinx.coroutines.async
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.launch
9 | import osp.leobert.android.pagertrackerdemo.index.IndexActivity
10 | import osp.leobert.android.tracker.pager.TrackedPager
11 |
12 | @TrackedPager(pagerPoint = "P_1")
13 | class MainActivity : BaseActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 | val tv: TextView = findViewById(R.id.tv)
18 |
19 | lifecycleScope.launch {
20 | async {
21 | delay(3000)
22 | startActivity(this@MainActivity)
23 | // IndexActivity.launch(this@MainActivity)
24 | finish()
25 | }
26 |
27 | async {
28 | delay(1500)
29 | "假装是splash页面"
30 | }.let {
31 | tv.text = it.await()
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_index.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
25 |
26 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/FragmentTrackerImpl.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleEventObserver
5 | import androidx.lifecycle.LifecycleOwner
6 | import osp.leobert.android.tracker.pager.PagerChainTracker.Companion.takeIfInstance
7 |
8 | /**
9 | * Package: osp.leobert.android.tracker.pager
10 | * Classname: FragmentTrackerImpl
11 | * Created by leobert on 2020/5/19.
12 | *
13 | * 理论上各种复杂情况都应该走手动模式,onStart中处理上报的,理论上都是当做单独的点,排除掉ViewPager中各种切换的情况。
14 | *
15 | * 考虑到便捷,会尝试下再定义一个标识接口,用于Fragment在多个ViewPager中到处切换
16 | */
17 | class FragmentTrackerImpl : LifecycleEventObserver {
18 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
19 |
20 | if (event == Lifecycle.Event.ON_START) {
21 | source.takeIfInstance()?.let {
22 | PagerChainTracker.helpFragmentStart(it)
23 | }
24 | }
25 |
26 |
27 | if (event == Lifecycle.Event.ON_DESTROY) {
28 | source.takeIfInstance()?.let {
29 | PagerChainTracker.helpFragmentDestroy(it)
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_posts_record.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/BuryPointContext.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker;
2 |
3 | import android.util.Pair;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 |
8 | import java.util.List;
9 |
10 | /**
11 | * 业务脱敏,大部分内容已被移除
12 | * Created by leobert on 2018/12/18.
13 | */
14 | public abstract class BuryPointContext {
15 |
16 |
17 | public void track(String pointKey) {
18 |
19 | }
20 |
21 | @SafeVarargs
22 | public final void track(String pointKey, Pair... appendData) {
23 |
24 | }
25 |
26 |
27 | @SafeVarargs
28 | public final void track(@NonNull BuryPoint point, Pair... appendData) {
29 |
30 | }
31 |
32 | @SafeVarargs
33 | public final void track(@NonNull BuryPoint point, boolean useParent, Pair... appendData) {
34 | }
35 |
36 | public final void track(@NonNull BuryPoint point, boolean useParent, @NonNull List> appendData) {
37 | }
38 |
39 |
40 | @Nullable
41 | public static BuryPointUploader buryPointUploader;
42 |
43 | public interface BuryPointUploader {
44 | void upload(String pointKey, List> params);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo
2 |
3 | import androidx.fragment.app.Fragment
4 | import osp.leobert.android.tracker.pager.ITrackedPager
5 | import osp.leobert.android.tracker.pager.PagerChainTracker
6 |
7 | /**
8 | * Package: osp.leobert.android.pagertrackerdemo
9 | * Project: PagerTrackerDemo
10 | * Classname: BaseFragment
11 | * Created by leobert on 2020/6/30.
12 | */
13 | abstract class BaseFragment : Fragment(), ITrackedPager {
14 | private var mPagerToken: String? = null
15 |
16 | override fun setPagerToken(pagerToken: String) {
17 | mPagerToken = pagerToken
18 | }
19 |
20 | override fun getPagerToken(): String = mPagerToken ?: ""
21 |
22 | private var firstOnResume = true
23 |
24 | override fun onResume() {
25 | super.onResume()
26 | if (firstOnResume) {
27 | firstOnResume = false
28 | PagerChainTracker.helpFragmentStart(this)
29 | } else {
30 | PagerChainTracker.helpFragmentOnResumeInViewPager(this)
31 | }
32 | }
33 |
34 | override fun onDestroy() {
35 | PagerChainTracker.helpFragmentDestroy(this)
36 | super.onDestroy()
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PagerTrackerDemo
3 | MainActivity
4 | Home
5 | Dashboard
6 | Notifications
7 | EssayDetailActivity
8 |
9 | First Fragment
10 | Second Fragment
11 | Next
12 | Previous
13 |
14 |
15 |
16 |
17 | 君不见,黄河之水天上来,奔流到海不复回。\n
18 | 君不见,高堂明镜悲白发,朝如青丝暮成雪。\n
19 | 人生得意须尽欢,莫使金樽空对月。\n
20 | 天生我材必有用,千金散尽还复来。\n
21 | 烹羊宰牛且为乐,会须一饮三百杯。\n
22 | 岑夫子,丹丘生,将进酒,杯莫停。\n
23 | 与君歌一曲,请君为我倾耳听。\n
24 | 钟鼓馔玉不足贵,但愿长醉不复醒。\n
25 | 古来圣贤皆寂寞,惟有饮者留其名。\n
26 | 陈王昔时宴平乐,斗酒十千恣欢谑。\n
27 | 主人何为言少钱,径须沽取对君酌。\n
28 | 五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。
29 |
30 | Hello second fragment. Arg: %1$s
31 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
14 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/index/ui/dashboard/DashboardFragment.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.index.ui.dashboard
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.lifecycle.Observer
9 | import androidx.lifecycle.ViewModelProviders
10 | import osp.leobert.android.pagertrackerdemo.BaseFragment
11 | import osp.leobert.android.pagertrackerdemo.R
12 | import osp.leobert.android.tracker.pager.ITrackedPager
13 | import osp.leobert.android.tracker.pager.TrackedPager
14 |
15 |
16 | @TrackedPager(
17 | pagerPoint = "P_DASH_FG",
18 | whenFragment = TrackedPager.FragmentStrategy.REPLACE_ACTIVITY
19 | )
20 | class DashboardFragment : BaseFragment(), ITrackedPager.FragmentInViewPager {
21 |
22 | private lateinit var dashboardViewModel: DashboardViewModel
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater,
26 | container: ViewGroup?,
27 | savedInstanceState: Bundle?
28 | ): View? {
29 | dashboardViewModel = ViewModelProviders.of(this).get(DashboardViewModel::class.java)
30 | val root = inflater.inflate(R.layout.fragment_demo, container, false)
31 | val textView: TextView = root.findViewById(R.id.tv)
32 | dashboardViewModel.text.observe(viewLifecycleOwner, Observer {
33 | textView.text = it
34 | })
35 | return root
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/index/ui/home/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.index.ui.home
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.lifecycle.Observer
9 | import androidx.lifecycle.ViewModelProviders
10 | import osp.leobert.android.pagertrackerdemo.BaseFragment
11 | import osp.leobert.android.pagertrackerdemo.R
12 | import osp.leobert.android.pagertrackerdemo.essay.EssayDetailActivity
13 | import osp.leobert.android.tracker.pager.ITrackedPager
14 | import osp.leobert.android.tracker.pager.TrackedPager
15 |
16 | @TrackedPager(pagerPoint = "P_INDEX_FG", whenFragment = TrackedPager.FragmentStrategy.REPLACE_ACTIVITY)
17 | class HomeFragment : BaseFragment(), ITrackedPager.FragmentInViewPager {
18 |
19 | private lateinit var homeViewModel: HomeViewModel
20 |
21 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
22 | homeViewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)
23 | val root = inflater.inflate(R.layout.fragment_demo, container, false)
24 | val textView: TextView = root.findViewById(R.id.tv)
25 | homeViewModel.text.observe(viewLifecycleOwner, Observer {
26 | textView.text = it
27 | })
28 |
29 | textView.setOnClickListener {
30 | EssayDetailActivity.launch(it.context, "321")
31 | }
32 |
33 | return root
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_essay_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/DemoApplication.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import com.google.gson.Gson
8 | import osp.leobert.android.tracker.BuryPointContext
9 | import osp.leobert.android.tracker.pager.PagerChainTracker
10 | import osp.leobert.android.tracker.pager.PagerTrackerLifecycleCallbacks
11 |
12 | /**
13 | * Package: osp.leobert.android.pagertrackerdemo
14 | * Project: PagerTrackerDemo
15 | * Classname: DemoApplication
16 | * Created by leobert on 2020/6/30.
17 | */
18 | class DemoApplication : Application() {
19 | companion object {
20 | val gson: Gson = Gson()
21 | }
22 |
23 | override fun onCreate() {
24 | super.onCreate()
25 | PagerChainTracker.debug = true
26 | BuryPointContext.buryPointUploader =
27 | BuryPointContext.BuryPointUploader { pointKey, params ->
28 | pointKey?.let {
29 | Log.d(
30 | PagerChainTracker.tag,
31 | "onPointUpload:$it , params: ${gson.toJson(params)}"
32 | )
33 | }
34 | }
35 | registerActivityLifecycleCallbacks(PagerTrackerLifecycleCallbacks())
36 | }
37 | }
38 |
39 | fun Intent.launchActivity(context: Context) {
40 | context.startActivity(this)
41 | }
42 |
43 | inline fun startActivity(context: Context) {
44 | Intent(context,T::class.java).launchActivity(context)
45 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PagerTrackerDemo
2 |
3 | 这仅仅是一个demo,演示了一套用户路径埋点统计方案,具体方案的探索和讨论见我的博客:
4 | [地址](https://blog.csdn.net/a774057695/article/details/106843586)
5 |
6 | demo中演示了一些典型用法,包含收集环节、链路维护环节、上报触发环节如:
7 |
8 | * 自动触发上报
9 | * 手动触发上报
10 | * 添加数据
11 | * fragment具备独立点替代activity页面点
12 | * fragment不具备独立点时不影响activity页面点
13 |
14 | 如果有疑惑可以提issue或者博客下评论,(但是可能因为工作原因回复没那么及时)
15 |
16 | 博客中提到了利用DummyPager来处理特殊情况的,如果有真实需求,可以提issue,目前demo中没有写。
17 |
18 | 通过日志观测结果:
19 |
20 | ```
21 | V/pager_track:
22 | V/pager_track:
23 | V/pager_track: print chain start========
24 | V/pager_track: 0:PagerEntity(pagerToken='MainActivity_0', pagerPoint='P_1', reserveLimit=1, reserveConfig={}, data=[Pair{frompage }], bpToken='')
25 | V/pager_track: 1:PagerEntity(pagerToken='IndexActivity_1', pagerPoint='P_INDEX_FG', reserveLimit=1, reserveConfig={}, data=[], bpToken='HomeFragment_2')
26 | V/pager_track: 2:PagerEntity(pagerToken='EssayDetailActivity_7', pagerPoint='P_ESSAY_DETAIL', reserveLimit=1, reserveConfig={}, data=[Pair{id 321}, Pair{frompage P_INDEX_FG}], bpToken='')
27 | V/pager_track: 3:PagerEntity(pagerToken='BioActivity_9', pagerPoint='P_BIO', reserveLimit=1, reserveConfig={}, data=[], bpToken='')
28 | V/pager_track: print chain end=====
29 | V/pager_track:
30 | V/pager_track:
31 | D/pager_track: onPointUpload:P_BIO , params: [{"first":"frompage","second":"P_ESSAY_DETAIL"}]
32 | D/pager_track: report pager:P_BIO,from: P_ESSAY_DETAIL, chain: P_1 - P_INDEX_FG - P_ESSAY_DETAIL - P_BIO
33 | ```
34 |
35 | 页面链路变更时会打印出链路详细状态(最新的页面在最后)
36 |
37 | onPointUpload 是上报的信息(模拟埋点上报)
38 |
39 | report pager 是库内部的简要debug信息,触发上报时会打印,给出上报的页面点号、来源页、简要的全页面链路信息。
40 |
41 | 另外:tracker是library,如果客观分析后该方案适合您使用,可以遵循MIT协议自行移植。
42 |
43 | **开源不易,觉得有帮助的朋友欢迎点个star或者给博客点个赞支持下**
44 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_essay_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
18 |
19 |
23 |
24 |
25 |
33 |
34 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/vh_notification.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
22 |
23 |
24 |
34 |
35 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/essay/EssayDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.essay
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.util.Pair
7 | import android.view.View
8 | import kotlinx.android.synthetic.main.content_essay_detail.*
9 | import osp.leobert.android.pagertrackerdemo.BaseActivity
10 | import osp.leobert.android.pagertrackerdemo.R
11 | import osp.leobert.android.pagertrackerdemo.bio.BioActivity
12 | import osp.leobert.android.pagertrackerdemo.launchActivity
13 | import osp.leobert.android.tracker.pager.PagerChainTracker
14 | import osp.leobert.android.tracker.pager.TrackedPager
15 |
16 | @TrackedPager(pagerPoint = "P_ESSAY_DETAIL", autoReport = false)
17 | class EssayDetailActivity : BaseActivity() {
18 |
19 | companion object {
20 | const val EXT_STR_ID = "EXT_STR_ID"
21 |
22 | fun launch(context: Context, essayId: String) {
23 | Intent(context, EssayDetailActivity::class.java)
24 | .putExtra(EXT_STR_ID, essayId)
25 | .launchActivity(context)
26 | }
27 | }
28 |
29 | private val essayId: String by lazy {
30 | intent.getStringExtra(EXT_STR_ID) ?: ""
31 | }
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | setContentView(R.layout.activity_essay_detail)
36 | setSupportActionBar(findViewById(R.id.toolbar))
37 |
38 |
39 | PagerChainTracker.pressData(this, mutableListOf(Pair("id", essayId)))
40 | PagerChainTracker.manualReport()
41 |
42 | val onAuthorClickedListener = View.OnClickListener {
43 | BioActivity.launch(this@EssayDetailActivity, "李白的id")
44 | }
45 | avatar.setOnClickListener(onAuthorClickedListener)
46 | tv_author.setOnClickListener(onAuthorClickedListener)
47 | }
48 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 |
8 | defaultConfig {
9 | applicationId "osp.leobert.android.pagertrackerdemo"
10 | minSdkVersion 21
11 | targetSdkVersion 29
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation fileTree(dir: "libs", include: ["*.jar"])
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
36 | implementation 'androidx.core:core-ktx:1.3.0'
37 | implementation 'androidx.appcompat:appcompat:1.1.0'
38 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
39 | implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
40 | implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
41 | implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
42 | testImplementation 'junit:junit:4.12'
43 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
45 |
46 | def paging_version = "3.0.0-alpha02"
47 |
48 | implementation "androidx.paging:paging-runtime:$paging_version"
49 | implementation project(':tracker')
50 | implementation 'com.google.code.gson:gson:2.8.5'
51 | implementation "androidx.viewpager2:viewpager2:1.0.0"
52 | implementation 'com.google.android.material:material:1.1.0'
53 |
54 | // // alternatively - without Android dependencies for tests
55 | // testImplementation "androidx.paging:paging-common:$paging_version"
56 | //
57 | // // optional - RxJava2 support
58 | // implementation "androidx.paging:paging-rxjava2:$paging_version"
59 | //
60 | // // optional - Guava ListenableFuture support
61 | // implementation "androidx.paging:paging-guava:$paging_version"
62 |
63 |
64 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/PagerEntity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager
2 |
3 | import android.util.Pair
4 |
5 | /**
6 | * Package: osp.leobert.android.tracker.pager
7 | * Classname: PagerEntity
8 | * Created by leobert on 2020/5/14.
9 | */
10 | class PagerEntity(val pagerToken: String, var pagerPoint: String = "未设置",
11 | internal var reserveLimit: Int = 1,
12 | val reserveConfig: MutableMap = hashMapOf(),
13 | var data: MutableList>,
14 | var bpToken: String = "") {
15 |
16 | companion object {
17 | // fun formatPagerChain(pagerChain: ArrayList): String {
18 | // var i = pagerChain.size - 1
19 | //
20 | // var chain = " - "
21 | //
22 | // var p: String? = null
23 | // while (i >= 0) {
24 | // chain = if (p == null) {
25 | // " - " + pagerChain[i].pagerPoint + chain
26 | // } else {
27 | // " - " + (pagerChain[i].reserveConfig[p] ?: pagerChain[i].pagerPoint) + chain
28 | // }
29 | // p = pagerChain[i].pagerPoint
30 | // i--
31 | // }
32 | // return chain.substring(0, chain.length - 3)
33 | // }
34 |
35 | fun formatPagerChain(pagerChain: ArrayList, limit: Int): Pair {
36 | var i = pagerChain.size - 1
37 |
38 | var chain = ""
39 | var subChain = ""
40 |
41 | var p: String? = null
42 | while (i >= 0) {
43 | val cur = if (p == null) {
44 | pagerChain[i].pagerPoint
45 | } else {
46 | pagerChain[i].reserveConfig[p] ?: pagerChain[i].pagerPoint
47 | }
48 |
49 | chain = " - $cur$chain"
50 |
51 |
52 | if (pagerChain.size - i - 1 in 1..limit) {
53 | subChain = " - $cur$subChain"
54 | }
55 |
56 | p = pagerChain[i].pagerPoint
57 | i--
58 | }
59 | val all = if (chain.length > 3) chain.substring(3) else chain
60 | subChain = if (subChain.length > 3) subChain.substring(3) else subChain
61 | return Pair(all, subChain)
62 | }
63 | }
64 |
65 | override fun toString(): String {
66 | return "PagerEntity(pagerToken='$pagerToken', pagerPoint='$pagerPoint', reserveLimit=$reserveLimit, reserveConfig=$reserveConfig, data=$data, bpToken='$bpToken')"
67 | }
68 | }
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/TrackedPager.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager;
2 |
3 | import android.app.Activity;
4 |
5 | import androidx.fragment.app.Fragment;
6 |
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.RetentionPolicy;
9 |
10 | /**
11 | * Package: osp.leobert.android.tracker.pager
12 | * Classname: TrackedPager
13 | * Created by leobert on 2020/5/14.
14 | */
15 | @Retention(RetentionPolicy.RUNTIME)
16 | public @interface TrackedPager {
17 | String pagerPoint();
18 |
19 | ReserveConfig[] reserveConfig() default {};
20 |
21 | boolean autoReport() default true;
22 |
23 | FragmentStrategy whenFragment() default FragmentStrategy.ALONE;
24 |
25 | /**
26 | * @return the max limit of pagers backing-retrieve tracking, tracking all if less than 1
27 | */
28 | int reserveLimit() default 1;
29 |
30 | // /**
31 | // * @return true if ignore the page in the chain,even it has implemented {@link ITrackedPager}
32 | // 目前通过忽略未配置的fragment处理,如果有必要再扩展这个配置,
33 | //目前用于activity,如果是true,不会加到chain中,也不会处理上报,
34 | // */
35 | boolean ignore() default false;
36 |
37 | enum FragmentStrategy {
38 | /**
39 | * 替代宿主Activity的点
40 | */
41 | REPLACE_ACTIVITY {
42 | @Override
43 | public void manualAddChainNode(ITrackedPager pager, boolean report) {
44 | if (pager instanceof Fragment) {
45 | Activity activity = ((Fragment) pager).getActivity();
46 | // Fragment parentFg = ((Fragment) pager).getParentFragment();
47 | // if (parentFg != null) {
48 | // Log.i(PagerChainTracker.tag, "a fragment's sub fragment should not use REPLACE,还没有想过这种如何设计", new Throwable());
49 | // super.manualAddChainNode(pager, report);
50 | // } else
51 | if (activity instanceof ITrackedPager) {
52 | PagerChainTracker.Companion.replacePagerInfo((ITrackedPager) activity, pager, report);
53 | } else
54 | super.manualAddChainNode(pager, report);
55 | } else
56 | super.manualAddChainNode(pager, report);
57 | }
58 | },
59 |
60 | // /**
61 | // * 作为宿主点的一部分信息,暂时没有使用场景,目前走手动塞入信息即可
62 | // */
63 | // INJECT,
64 |
65 | /**
66 | * 作为一个独立的新点
67 | */
68 | ALONE;
69 |
70 | public void manualAddChainNode(ITrackedPager pager, boolean report) {
71 | PagerChainTracker.Companion.manualAddChainNode(PagerChainTracker.Companion.createPagerEntity(pager), report);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/BuryPoint.java:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker;
2 |
3 | import android.util.Pair;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * 业务脱敏,大部分内容已被移除
11 | * Created by leobert on 2018/12/19.
12 | */
13 | public abstract class BuryPoint {
14 |
15 | public static BuryPoint normal(@NonNull String key) {
16 | return new NormalBuryPoint(key);
17 | }
18 |
19 | public static BuryPoint transfer(@NonNull String key) {
20 | return new TransferBuryPoint(key);
21 | }
22 |
23 | @NonNull
24 | private String key;
25 |
26 | @NonNull
27 | public String getKey() {
28 | return key;
29 | }
30 |
31 | BuryPoint(@NonNull String key) {
32 | this.key = key;
33 | }
34 |
35 | void handle(@NonNull BuryPointContext context, Pair[] appendData) {
36 | }
37 |
38 | void handle(@NonNull BuryPointContext context, List> appendData) {
39 |
40 | }
41 |
42 | void allocate(@NonNull BuryPointContext context, @NonNull List> appendData) {
43 | }
44 |
45 |
46 | @Override
47 | public boolean equals(Object o) {
48 | if (this == o) return true;
49 | if (o == null || getClass() != o.getClass()) return false;
50 |
51 | BuryPoint buryPoint = (BuryPoint) o;
52 |
53 | return key.equals(buryPoint.key);
54 | }
55 |
56 | @Override
57 | public int hashCode() {
58 | return key.hashCode();
59 | }
60 |
61 | /**
62 | * 普通点,不会派生其他点
63 | */
64 | public static class NormalBuryPoint extends BuryPoint {
65 | NormalBuryPoint(@NonNull String key) {
66 | super(key);
67 | }
68 | }
69 |
70 | /**
71 | * 本身点名无法确定,key只是一个事件唯一标识,根据上下文处理成真正的点
72 | */
73 | public static class TransferBuryPoint extends BuryPoint {
74 |
75 | TransferBuryPoint(@NonNull String key) {
76 | super(key);
77 | }
78 |
79 | @Override
80 | void handle(@NonNull BuryPointContext context, Pair[] appendData) {
81 | }
82 |
83 | void handle(@NonNull BuryPointContext context, List> appendData) {
84 | }
85 |
86 | @Override
87 | void allocate(@NonNull BuryPointContext context, @NonNull List> appendData) {
88 | super.allocate(context, appendData);
89 | }
90 | }
91 |
92 | /**
93 | * 可派生的点,慎用,目前我能想到的场景都可以使用TransferBuryPoint 构建出多个NormalBuryPoint处理
94 | * // * @hide
95 | */
96 | private static class FissileBuryPoint extends BuryPoint {
97 |
98 | public FissileBuryPoint(@NonNull String key) {
99 | super(key);
100 | }
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/index/ui/notifications/NotificationsFragment.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.index.ui.notifications
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.lifecycle.Observer
9 | import androidx.lifecycle.ViewModelProviders
10 | import androidx.recyclerview.widget.LinearLayoutManager
11 | import androidx.recyclerview.widget.RecyclerView
12 | import kotlinx.android.synthetic.main.fragment_demo_notify.*
13 | import osp.leobert.android.pagertrackerdemo.BaseFragment
14 | import osp.leobert.android.pagertrackerdemo.R
15 | import osp.leobert.android.pagertrackerdemo.bio.BioActivity
16 | import osp.leobert.android.tracker.pager.ITrackedPager
17 | import osp.leobert.android.tracker.pager.TrackedPager
18 |
19 |
20 | @TrackedPager(pagerPoint = "P_NOTIFY_FG", whenFragment = TrackedPager.FragmentStrategy.REPLACE_ACTIVITY)
21 | class NotificationsFragment : BaseFragment(), ITrackedPager.FragmentInViewPager {
22 |
23 | private lateinit var notificationsViewModel: NotificationsViewModel
24 |
25 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
26 | notificationsViewModel = ViewModelProviders.of(this).get(NotificationsViewModel::class.java)
27 | return inflater.inflate(R.layout.fragment_demo_notify, container, false)
28 | }
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | super.onViewCreated(view, savedInstanceState)
32 | notification_rv.layoutManager = LinearLayoutManager(notification_rv.context)
33 | notificationsViewModel.notifications.observe(viewLifecycleOwner, Observer {
34 | notification_rv.adapter = NotificationsAdapter(it)
35 | })
36 | }
37 | }
38 |
39 | class NotificationVH(parent: ViewGroup) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.vh_notification, parent, false)) {
40 |
41 | val tvName = itemView.findViewById(R.id.tv_user)
42 | val tvMsg = itemView.findViewById(R.id.tv_msg)
43 | var notification: Notification? = null
44 |
45 | init {
46 |
47 | itemView.setOnClickListener { v ->
48 | notification?.let {
49 | BioActivity.launch(v.context, it.userId)
50 | }
51 | }
52 | }
53 |
54 | fun bind(notification: Notification) {
55 | this.notification = notification
56 | tvName.text = notification.userName
57 | tvMsg.text = notification.message
58 | }
59 |
60 | }
61 |
62 | class NotificationsAdapter(val list: List) : RecyclerView.Adapter() {
63 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationVH = NotificationVH(parent)
64 |
65 | override fun getItemCount(): Int = list.size
66 |
67 | override fun onBindViewHolder(holder: NotificationVH, position: Int) {
68 | holder.bind(list[position])
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/PagerTrackerLifecycleCallbacks.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager
2 |
3 | import android.app.Activity
4 | import android.app.Application.ActivityLifecycleCallbacks
5 | import android.os.Bundle
6 | import android.util.Log
7 | import osp.leobert.android.tracker.pager.PagerChainTracker.Companion.takeIfInstance
8 |
9 | /**
10 | * Package: osp.leobert.android.tracker.pager
11 | * Classname: PagerTrackerLifecycleCallbacks
12 | * Created by leobert on 2020/5/14.
13 | */
14 | class PagerTrackerLifecycleCallbacks : ActivityLifecycleCallbacks {
15 | val tag = PagerChainTracker.tag
16 |
17 | private class DebugImpl:ArrayList() {
18 | override fun add(element: PagerEntity): Boolean {
19 | return super.add(element).apply {
20 | PagerChainTracker.printChain()
21 | }
22 | }
23 | }
24 |
25 | private val pagerChain: ArrayList = if (PagerChainTracker.debug) DebugImpl() else arrayListOf()
26 |
27 | private val dataBundle: PagerChainTracker.DataBundle = object : PagerChainTracker.DataBundle {
28 | override fun pagerChain(): ArrayList {
29 | return pagerChain
30 | }
31 | }
32 |
33 | init {
34 | PagerChainTracker.dataBundle = dataBundle
35 | }
36 |
37 |
38 | private val bundle_key_str_pager_token = "bundle_key_str_pager_token"
39 |
40 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
41 | activity.takeIfInstance()?.let {
42 | if (savedInstanceState == null) {
43 | val trackedPager = activity.javaClass.getAnnotation(TrackedPager::class.java)
44 | if (trackedPager?.ignore == true)
45 | return
46 |
47 | pagerChain.add(PagerChainTracker.createPagerEntity(it))
48 | PagerChainTracker.findBpContext(it)
49 |
50 | //auto or manual report
51 | trackedPager?.takeIf { tp -> tp.autoReport }?.let { _ ->
52 | PagerChainTracker.handleReport(it, pagerChain)
53 | }
54 |
55 | } else {
56 | savedInstanceState.getString(bundle_key_str_pager_token)?.let { token ->
57 | it.pagerToken = token
58 | }
59 | PagerChainTracker.findBpContext(it)
60 | }
61 |
62 | //先不搞这个,有风险
63 | // if (activity is FragmentActivity) {
64 | // val tmp = activity.supportFragmentManager
65 | // //可能会影响到一些系统的默认处理
66 | // tmp.fragmentFactory = FragmentFactoryWrapper(tmp.fragmentFactory)
67 | // } else {
68 | // Log.i(tag,"如有必要,它使用的Fragment需要单独配置生命周期检测,$activity")
69 | // }
70 | }
71 | }
72 |
73 | override fun onActivityStarted(activity: Activity) {
74 | }
75 |
76 | override fun onActivityResumed(activity: Activity) {
77 | activity.takeIfInstance()?.let {
78 | it.pagerToken.let { token ->
79 | var i = pagerChain.size - 1
80 | while (i >= 0 && pagerChain[i].pagerToken != token) {
81 | i--
82 | }
83 |
84 | while (i >= 0 && i < pagerChain.size - 1) {
85 | pagerChain.removeAt(pagerChain.size - 1)
86 | }
87 | }
88 | }
89 | }
90 |
91 | override fun onActivityPaused(activity: Activity) {
92 | }
93 |
94 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
95 | activity.takeIfInstance()?.let {
96 | outState.putString(bundle_key_str_pager_token, it.pagerToken)
97 | }
98 | }
99 |
100 | override fun onActivityStopped(activity: Activity) {
101 | }
102 |
103 | override fun onActivityDestroyed(activity: Activity) {
104 | activity.takeIfInstance()?.let {
105 | it.pagerToken.let { token ->
106 | if (pagerChain.size > 1 && pagerChain[pagerChain.size - 1].pagerToken == token) {
107 | pagerChain.removeAt(pagerChain.size - 1)
108 | } else {
109 | Log.i(tag, "want to remove last:${PagerChainTracker.debugPagerInfo(it)},but size is ${pagerChain.size} and last is ${pagerChain.lastOrNull()}")
110 | }
111 | PagerChainTracker.unRegisterBp(token)
112 | }
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/index/IndexActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.index
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.os.PersistableBundle
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.FragmentActivity
9 | import androidx.fragment.app.FragmentManager
10 | import androidx.lifecycle.Lifecycle
11 | import androidx.viewpager2.adapter.FragmentStateAdapter
12 | import androidx.viewpager2.widget.ViewPager2
13 | import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
14 | import com.google.android.material.tabs.TabLayout
15 | import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
16 | import osp.leobert.android.pagertrackerdemo.BaseActivity
17 | import osp.leobert.android.pagertrackerdemo.R
18 | import osp.leobert.android.pagertrackerdemo.index.ui.dashboard.DashboardFragment
19 | import osp.leobert.android.pagertrackerdemo.index.ui.home.HomeFragment
20 | import osp.leobert.android.pagertrackerdemo.index.ui.notifications.NotificationsFragment
21 | import osp.leobert.android.pagertrackerdemo.launchActivity
22 | import osp.leobert.android.tracker.pager.PagerChainTracker
23 | import osp.leobert.android.tracker.pager.TrackedPager
24 |
25 | //首页需要Fragment的P点,所以不再单独定义点号,拦截自动上传即可
26 | @TrackedPager(pagerPoint = "",autoReport = false)
27 | class IndexActivity : BaseActivity() {
28 | companion object {
29 | fun launch(context: Context) {
30 | Intent(context, IndexActivity::class.java).launchActivity(context)
31 | }
32 | }
33 |
34 | private var mViewPager2: ViewPager2? = null
35 | private var mTabLayout: TabLayout? = null
36 | private var mAdapter: ViewPagerFragmentStateAdapter? = null
37 |
38 | private val key_index = "key_index"
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | setContentView(R.layout.activity_index)
43 |
44 | mTabLayout = findViewById(R.id.tablayout)
45 | mViewPager2 = findViewById(R.id.viewpager2)
46 | mAdapter = ViewPagerFragmentStateAdapter(supportFragmentManager, lifecycle)
47 |
48 | mViewPager2?.let {
49 | it.offscreenPageLimit = 1
50 | it.adapter = mAdapter
51 |
52 | it.registerOnPageChangeCallback(object : OnPageChangeCallback() {
53 | override fun onPageSelected(position: Int) {
54 | super.onPageSelected(position)
55 | mTabLayout?.setScrollPosition(position, 0f, false)
56 | }
57 | })
58 | }
59 |
60 | mTabLayout?.let {
61 |
62 | it.addTab(it.newTab().setText("Index"))
63 | it.addTab(it.newTab().setText("Notify"))
64 | it.addTab(it.newTab().setText("Dash"))
65 |
66 | it.addOnTabSelectedListener(object : OnTabSelectedListener {
67 | override fun onTabSelected(tab: TabLayout.Tab) {
68 | mViewPager2?.currentItem = tab.position
69 | }
70 |
71 | override fun onTabUnselected(tab: TabLayout.Tab) {}
72 | override fun onTabReselected(tab: TabLayout.Tab) {
73 | if (mViewPager2?.currentItem != tab.position) {
74 | mViewPager2?.currentItem = tab.position
75 | }
76 | }
77 | })
78 | }
79 | }
80 |
81 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
82 | super.onRestoreInstanceState(savedInstanceState)
83 | val index = savedInstanceState.getInt(key_index)
84 | mTabLayout?.setScrollPosition(index, 0f, true)
85 | mViewPager2?.currentItem = index
86 | }
87 |
88 | override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
89 | outState.putInt(key_index, mTabLayout?.selectedTabPosition ?: 0)
90 | super.onSaveInstanceState(outState, outPersistentState)
91 | }
92 |
93 | override fun finish() {
94 | //对于一般的应用,主页面关闭基本就可以认为页面链全部清除了。
95 | PagerChainTracker.clearAll()
96 | super.finish()
97 | }
98 |
99 |
100 | internal class ViewPagerFragmentStateAdapter : FragmentStateAdapter {
101 | constructor(fragmentActivity: FragmentActivity) : super(fragmentActivity)
102 | constructor(
103 | fragmentManager: FragmentManager,
104 | lifecycle: Lifecycle
105 | ) : super(fragmentManager, lifecycle)
106 |
107 | override fun createFragment(position: Int): Fragment {
108 | return when (position) {
109 | 0 -> HomeFragment()
110 | 1 -> NotificationsFragment()
111 | else -> DashboardFragment()
112 | }
113 | }
114 |
115 | override fun getItemCount(): Int {
116 | return 3
117 | }
118 | }
119 | }
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/java/osp/leobert/android/pagertrackerdemo/bio/PostsRecordActivity.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.pagertrackerdemo.bio
2 |
3 | import android.os.Bundle
4 | import android.os.PersistableBundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.RelativeLayout
9 | import android.widget.TextView
10 | import androidx.fragment.app.Fragment
11 | import androidx.fragment.app.FragmentActivity
12 | import androidx.fragment.app.FragmentManager
13 | import androidx.lifecycle.Lifecycle
14 | import androidx.viewpager2.adapter.FragmentStateAdapter
15 | import androidx.viewpager2.widget.ViewPager2
16 | import com.google.android.material.tabs.TabLayout
17 | import osp.leobert.android.pagertrackerdemo.BaseActivity
18 | import osp.leobert.android.pagertrackerdemo.R
19 | import osp.leobert.android.tracker.pager.TrackedPager
20 |
21 | /**
22 | * 演示Fragment不需要页面点的情况。
23 | * */
24 | @TrackedPager(pagerPoint = "P_POSTS_RECORD")
25 | class PostsRecordActivity : BaseActivity() {
26 |
27 | private var mViewPager2: ViewPager2? = null
28 | private var mTabLayout: TabLayout? = null
29 | private var mAdapter: ViewPagerFragmentStateAdapter? = null
30 |
31 | private val key_index = "key_index"
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | setContentView(R.layout.activity_posts_record)
36 | mTabLayout = findViewById(R.id.tablayout)
37 | mViewPager2 = findViewById(R.id.viewpager2)
38 | mAdapter = ViewPagerFragmentStateAdapter(supportFragmentManager, lifecycle)
39 |
40 | mViewPager2?.let {
41 | it.offscreenPageLimit = 1
42 | it.adapter = mAdapter
43 |
44 | it.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
45 | override fun onPageSelected(position: Int) {
46 | super.onPageSelected(position)
47 | mTabLayout?.setScrollPosition(position, 0f, false)
48 | }
49 | })
50 | }
51 |
52 | mTabLayout?.let {
53 |
54 | it.addTab(it.newTab().setText("我发布的文章"))
55 | it.addTab(it.newTab().setText("我发布的动态"))
56 |
57 | it.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
58 | override fun onTabSelected(tab: TabLayout.Tab) {
59 | mViewPager2?.currentItem = tab.position
60 | }
61 |
62 | override fun onTabUnselected(tab: TabLayout.Tab) {}
63 | override fun onTabReselected(tab: TabLayout.Tab) {
64 | if (mViewPager2?.currentItem != tab.position) {
65 | mViewPager2?.currentItem = tab.position
66 | }
67 | }
68 | })
69 | }
70 | }
71 |
72 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
73 | super.onRestoreInstanceState(savedInstanceState)
74 | val index = savedInstanceState.getInt(key_index)
75 | mTabLayout?.setScrollPosition(index, 0f, true)
76 | mViewPager2?.currentItem = index
77 | }
78 |
79 | override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
80 | outState.putInt(key_index, mTabLayout?.selectedTabPosition ?: 0)
81 | super.onSaveInstanceState(outState, outPersistentState)
82 | }
83 |
84 | }
85 |
86 | private class DemoFragment : Fragment() {
87 | companion object {
88 | const val BUNDLE_STR_PAGE_NAME = "BUNDLE_STR_PAGE_NAME"
89 |
90 | fun newInstance(name: String): Fragment {
91 | return DemoFragment().apply {
92 | arguments = Bundle().apply {
93 | putString(BUNDLE_STR_PAGE_NAME, name)
94 | }
95 | }
96 | }
97 | }
98 |
99 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
100 | val rl = RelativeLayout(inflater.context)
101 | rl.addView(TextView(inflater.context).apply {
102 | id = R.id.id_tv
103 | }, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
104 | return rl
105 | }
106 |
107 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
108 | super.onViewCreated(view, savedInstanceState)
109 | val tv = view.findViewById(R.id.id_tv)
110 | tv.text = arguments?.getString(BUNDLE_STR_PAGE_NAME) ?: "missing name"
111 |
112 | }
113 | }
114 |
115 | class ViewPagerFragmentStateAdapter : FragmentStateAdapter {
116 | constructor(fragmentActivity: FragmentActivity) : super(fragmentActivity)
117 | constructor(fragmentManager: FragmentManager, lifecycle: Lifecycle) : super(fragmentManager, lifecycle)
118 |
119 | override fun createFragment(position: Int): Fragment {
120 | return when (position) {
121 | 0 -> DemoFragment.newInstance("我发布的文章")
122 | else -> DemoFragment.newInstance("我发布的动态")
123 | }
124 | }
125 |
126 | override fun getItemCount(): Int {
127 | return 2
128 | }
129 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/tracker/src/main/java/osp/leobert/android/tracker/pager/PagerChainTracker.kt:
--------------------------------------------------------------------------------
1 | package osp.leobert.android.tracker.pager
2 |
3 | import android.util.Log
4 | import android.util.Pair
5 | import androidx.fragment.app.Fragment
6 | import osp.leobert.android.tracker.BuryPoint
7 | import osp.leobert.android.tracker.BuryPointContext
8 | import java.util.concurrent.atomic.AtomicInteger
9 |
10 | /**
11 | * Package: osp.leobert.android.tracker.pager
12 | * Classname: PagerChainTracker
13 | * Created by leobert on 2020/5/15.
14 | */
15 | @Suppress("WeakerAccess", "unused")
16 | class PagerChainTracker {
17 | internal interface DataBundle {
18 | fun pagerChain(): ArrayList
19 | }
20 |
21 | companion object {
22 |
23 | open class DummyPage() : ITrackedPager {
24 | var mPagerToken: String? = null
25 |
26 | override fun setPagerToken(pagerToken: String) {
27 | this.mPagerToken = pagerToken
28 | }
29 |
30 | override fun getPagerToken(): String = mPagerToken ?: ""
31 |
32 | fun onCreate() {
33 | val trackedPager = javaClass.getAnnotation(TrackedPager::class.java)
34 | if (trackedPager?.ignore == true)
35 | return
36 |
37 | dataBundle?.pagerChain()?.let { pagerChain ->
38 | pagerChain.add(createPagerEntity(this))
39 | findBpContext(this)
40 |
41 | //auto or manual report
42 | trackedPager?.takeIf { tp -> tp.autoReport }?.let { _ ->
43 | handleReport(this, pagerChain)
44 | }
45 | }
46 | }
47 |
48 | fun onResume() {
49 | dataBundle?.pagerChain()?.let { pagerChain ->
50 | pagerToken.let { token ->
51 | var i = pagerChain.size - 1
52 | while (i >= 0 && pagerChain[i].pagerToken != token) {
53 | i--
54 | }
55 |
56 | while (i >= 0 && i < pagerChain.size - 1) {
57 | pagerChain.removeAt(pagerChain.size - 1)
58 | }
59 | }
60 | }
61 | }
62 |
63 | fun autoHandle() {
64 | if (pagerToken.isEmpty()) {
65 | onCreate()
66 | } else {
67 | onResume()
68 | }
69 | }
70 |
71 | }
72 |
73 | var debug = true
74 | private val bpContexts: MutableMap = hashMapOf()
75 |
76 | fun registerBp(token: String, bp: BuryPointContext) {
77 | bpContexts[token] = bp
78 | }
79 |
80 | fun unRegisterBp(token: String) {
81 | bpContexts.remove(token)
82 | }
83 |
84 | internal inline fun Any?.takeIfInstance(): R? {
85 | if (this is R) return this
86 | return null
87 | }
88 |
89 | private const val handleMissingNotatedFragment = false
90 |
91 | const val tag = "pager_track"
92 | private val atomicInteger = AtomicInteger(0)
93 |
94 | internal var dataBundle: DataBundle? = null
95 | get() {
96 | if (field == null)
97 | Log.d(tag, "dataBundle is null!")
98 | return field
99 | }
100 |
101 | //manual track
102 | fun track(pager: ITrackedPager) {
103 | dataBundle?.let {
104 | handleReport(pager, it.pagerChain())
105 | }
106 | }
107 |
108 | internal fun findBpContext(pager: ITrackedPager) {
109 | try {
110 | val methods = pager.javaClass.declaredMethods
111 | methods.forEach {
112 | if (it.getAnnotation(TrackerBpContext::class.java) != null) {
113 | it.isAccessible = true
114 | registerBp(pager.pagerToken, it.invoke(pager) as BuryPointContext)
115 | return@forEach
116 | }
117 | }
118 | } catch (e: Exception) {
119 | Log.e(tag, "findBpContext error", e)
120 | }
121 | }
122 |
123 | fun helpFragmentStart(pager: ITrackedPager) {
124 | try {
125 | dataBundle?.let { bundle ->
126 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
127 | when {
128 | trackedPager != null -> {
129 | pager.pagerToken.takeIf { it.isNotEmpty() }?.let { unRegisterBp(it) }
130 | pager.pagerToken = createToken(pager)
131 | findBpContext(pager)
132 | trackedPager.whenFragment.manualAddChainNode(pager, trackedPager.autoReport)
133 | }
134 | handleMissingNotatedFragment -> {
135 | bundle.pagerChain().add(createPagerEntity(pager))
136 | }
137 | else -> {
138 | Log.v(tag, "not notated for fragment: ${debugPagerInfo(pager)}")
139 | }
140 | }
141 | }
142 | } catch (e: Exception) {
143 | Log.e(tag, "error", e)
144 | }
145 | }
146 |
147 | //这里我们相信业务不会瞎来,不做严格的校验,简单校验后添加到链路中
148 | fun helpFragmentOnResumeInViewPager(pager: ITrackedPager) {
149 | try {
150 | pager.takeIfInstance()?.let {
151 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
152 | if (trackedPager?.whenFragment == TrackedPager.FragmentStrategy.REPLACE_ACTIVITY) {
153 | TrackedPager.FragmentStrategy.REPLACE_ACTIVITY.manualAddChainNode(it, false/*trackedPager.autoReport*/)
154 | } else {
155 | Log.e(tag, "当前仅处理${TrackedPager.FragmentStrategy.REPLACE_ACTIVITY}的配置")
156 | }
157 | }
158 | } catch (e: Exception) {
159 | Log.e(tag, "error", e)
160 | }
161 | }
162 |
163 | fun helpFragmentDestroy(pager: ITrackedPager) {
164 | try {
165 | pager.pagerToken.let { token ->
166 | dataBundle?.pagerChain()?.let { pagerChain ->
167 | pager.pagerToken.takeIf { it.isNotEmpty() }?.let { unRegisterBp(it) }
168 |
169 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
170 | if (trackedPager != null || handleMissingNotatedFragment) {
171 | if (pagerChain.size > 1 && pagerChain[pagerChain.size - 1].pagerToken == token) {
172 | pagerChain.removeAt(pagerChain.size - 1)
173 | } else {
174 | Log.i(tag, "want to remove last:${debugPagerInfo(pager)},but size is ${pagerChain.size} and last is ${pagerChain.lastOrNull()}")
175 | }
176 | } else {
177 | Log.v(tag, "not notated for fragment: ${debugPagerInfo(pager)}")
178 | }
179 | }
180 | }
181 | } catch (e: Exception) {
182 | Log.e(tag, "error", e)
183 | }
184 | }
185 |
186 | fun replacePagerInfo(pager: ITrackedPager, from: ITrackedPager, report: Boolean = false) {
187 | try {
188 | dataBundle?.let { bundle ->
189 | bundle.pagerChain().find { it.pagerToken == pager.pagerToken }?.let {
190 | val target = createPagerEntity(from, from.pagerToken.isEmpty())
191 | it.pagerPoint = target.pagerPoint
192 | it.reserveConfig.clear()
193 | it.reserveConfig.putAll(target.reserveConfig)
194 | it.data.clear()
195 | it.bpToken = from.pagerToken
196 |
197 | if (report)
198 | handleReport(pager, pagerChain = bundle.pagerChain())
199 | }
200 | }
201 | } catch (e: Exception) {
202 | Log.e(tag, "error", e)
203 | }
204 | }
205 |
206 | //慎用!
207 | fun replacePagerInfo(pager: ITrackedPager, from: PagerEntity, report: Boolean = false) {
208 | try {
209 | dataBundle?.let { bundle ->
210 | bundle.pagerChain().find { it.pagerToken == pager.pagerToken }?.let {
211 | it.pagerPoint = from.pagerPoint
212 | it.reserveConfig.clear()
213 | it.reserveConfig.putAll(from.reserveConfig)
214 | it.data.clear()
215 | it.bpToken = from.pagerToken
216 |
217 | if (report)
218 | handleReport(pager, pagerChain = bundle.pagerChain())
219 | }
220 | }
221 | } catch (e: Exception) {
222 | Log.e(tag, "error", e)
223 | }
224 | }
225 |
226 | /**
227 | * 手动添加一个节点,e.g.:弹窗显示
228 | * [report] true if report the point right now
229 | * */
230 | fun manualAddChainNode(pagerEntity: PagerEntity, report: Boolean = false) {
231 | try {
232 | dataBundle?.let {
233 | it.pagerChain().add(pagerEntity)
234 | if (report)
235 | reportPagers(it.pagerChain())
236 | }
237 | } catch (e: Exception) {
238 | Log.e(tag, "error", e)
239 | }
240 | }
241 |
242 | fun manualAddChainNode(pager: ITrackedPager, report: Boolean = false) {
243 | try {
244 | dataBundle?.let {
245 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
246 | if (trackedPager != null && trackedPager.autoReport && report) {
247 | val stackInfo = Throwable()
248 | Log.i(tag, "[bug?] " + debugPagerInfo(pager) + " is auto report, but manual report is called!", stackInfo)
249 | }
250 |
251 | if (trackedPager != null && pager is Fragment) {
252 | trackedPager.whenFragment.manualAddChainNode(pager, report)
253 | } else {
254 | manualAddChainNode(createPagerEntity(pager), report)
255 | }
256 | }
257 | } catch (e: Exception) {
258 | Log.e(tag, "error", e)
259 | }
260 | }
261 |
262 | private fun createToken(obj: Any): String {
263 | return obj.javaClass.simpleName + "_" + atomicInteger.getAndIncrement()
264 | }
265 |
266 | fun createPagerEntity(pager: ITrackedPager): PagerEntity {
267 | return createPagerEntity(pager, true)
268 | }
269 |
270 | fun createPagerEntity(pager: ITrackedPager, newToken: Boolean = true): PagerEntity {
271 | val token = if (newToken) createToken(pager) else pager.pagerToken
272 |
273 | val pagerEntity = PagerEntity(pagerToken = token, data = arrayListOf())
274 |
275 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
276 | if (trackedPager != null) {
277 | pagerEntity.pagerPoint = trackedPager.pagerPoint.takeIf { pn -> pn.isNotEmpty() }
278 | ?: "未设置(${pager.javaClass.simpleName})"
279 |
280 | trackedPager.reserveConfig.forEach { config ->
281 | pagerEntity.reserveConfig[config.on] = config.asPoint
282 | }
283 |
284 | pagerEntity.reserveLimit = trackedPager.reserveLimit
285 | } else {
286 | pagerEntity.pagerPoint = "未设置(${pager.javaClass.simpleName})"
287 | }
288 | pager.pagerToken = token
289 | return pagerEntity
290 | }
291 |
292 | /**
293 | * 以A->B->C为例,当前在C,注意调用时机至少是onCreate,准确的说,是页面被采集入栈,注意手动入栈的情况(例如文章详情页,可能P点是不一致的,就需要手动入栈)
294 | * limit 一般取1就可以了,向前面倒推的页面数量。如果是2,会返回 "A - B",如果<1,会返回全部页面链,
295 | * 返回的Pair,first是全部页面链,second是按照数量倒推的页面,e.g. limit=1 return:["A - B -C","B"],limit =2: ["A - B - C","A - B"]
296 | * */
297 | fun currentPagerChain(limit: Int = 1): Pair {
298 | try {
299 | dataBundle?.let {
300 | val chain = it.pagerChain()
301 | if (chain.size > 0) {
302 | return PagerEntity.formatPagerChain(chain, if (limit < 1) Int.MAX_VALUE else limit)
303 | }
304 | }
305 | } catch (e: Exception) {
306 | Log.e(tag, "error", e)
307 | }
308 | return Pair("", "")
309 | }
310 |
311 | fun pressData(pager: ITrackedPager, data: MutableList>) {
312 | try {
313 | dataBundle?.let {
314 | it.pagerChain().forEach { e ->
315 | if (e.pagerToken == pager.pagerToken) {
316 | //先全部添加,如果有必要,我们做差分
317 | e.data.addAll(data)
318 | }
319 | }
320 | }
321 | } catch (e: Exception) {
322 | Log.e(tag, "error", e)
323 | }
324 | }
325 |
326 | fun clearAll() {
327 | try {
328 | dataBundle?.pagerChain()?.clear()
329 | } catch (e: Exception) {
330 | Log.e(tag, "error", e)
331 | }
332 | }
333 |
334 | internal fun handleReport(pager: ITrackedPager, pagerChain: ArrayList) {
335 | //初期我们搞点log验证下,方便查bug,设计中,是可以直接上报了
336 | //毕竟没啥正常场景会出现:下一个页面都加载了,上一个页面才报页面点
337 | pager.pagerToken.let { token ->
338 | if (pagerChain.size < 1 || pagerChain[pagerChain.size - 1].pagerToken != token) {
339 | Log.d(tag, "[bug?] ${debugPagerInfo(pager)} want report pager point," +
340 | " but size is ${pagerChain.size} and last is ${pagerChain.lastOrNull()} ")
341 | }
342 |
343 | if (pagerChain.size > 0 /*&& pagerChain[pagerChain.size - 1].pagerToken == token*/) {
344 | reportPagers(pagerChain)
345 | }
346 | }
347 | }
348 |
349 | /**
350 | * 假定当前的页面已经入栈了,这一点需要业务自己控制了!
351 | * */
352 | fun manualReport() {
353 | dataBundle?.let {
354 | reportPagers(it.pagerChain())
355 | }
356 | }
357 |
358 | private fun reportPagers(pagerChain: ArrayList) {
359 | try {
360 | if (pagerChain.isEmpty()) {
361 | Log.e(tag, "try report pager chain but is empty")
362 | }
363 |
364 | val limit = pagerChain[pagerChain.size - 1].reserveLimit
365 |
366 | val chain = PagerEntity.formatPagerChain(pagerChain, if (limit < 1) Int.MAX_VALUE else limit)
367 | val lastNode = pagerChain[pagerChain.size - 1]
368 |
369 | val bpToken = lastNode.bpToken.takeIf { it.isNotEmpty() } ?: lastNode.pagerToken
370 |
371 | val bpContext = bpContexts[bpToken]
372 | if (bpContext != null) {
373 | bpContext.track(BuryPoint.normal(lastNode.pagerPoint), true, pagerChain[pagerChain.size - 1].data.apply {
374 | //this.add(Pair("chain", chain.first))
375 | this.add(Pair("frompage", chain.second))
376 | })
377 | } else {
378 | uploadPoint(lastNode.pagerPoint, pagerChain[pagerChain.size - 1].data.apply {
379 | //this.add(Pair("chain", chain.first))
380 | this.add(Pair("frompage", chain.second))
381 | })
382 | }
383 |
384 |
385 | Log.d(tag, "report pager:${pagerChain[pagerChain.size - 1].pagerPoint},from: ${chain.second}, chain: ${chain.first}")
386 | } catch (e: Exception) {
387 | Log.e(tag, "report pager", e)
388 | }
389 | }
390 |
391 | internal fun debugPagerInfo(pager: ITrackedPager): String {
392 | val pagerEntity = PagerEntity(pagerToken = "token", data = arrayListOf())
393 |
394 | val trackedPager = pager.javaClass.getAnnotation(TrackedPager::class.java)
395 | if (trackedPager != null) {
396 | pagerEntity.pagerPoint = trackedPager.pagerPoint.takeIf { pn -> pn.isNotEmpty() } ?: "未设置(${pager.javaClass.simpleName})"
397 |
398 | trackedPager.reserveConfig.forEach { config ->
399 | pagerEntity.reserveConfig[config.on] = config.asPoint
400 | }
401 | }
402 | return "${pager.javaClass.simpleName} $pagerEntity"
403 | }
404 |
405 | internal fun printChain() {
406 | Log.v(tag, " ")
407 | Log.v(tag, " ")
408 | Log.v(tag, "print chain start========")
409 | dataBundle?.pagerChain()?.forEachIndexed { i, e ->
410 | Log.v(tag, "$i:${e}")
411 | }
412 |
413 | Log.v(tag, "print chain end=====")
414 | Log.v(tag, " ")
415 | Log.v(tag, " ")
416 | }
417 |
418 | private fun uploadPoint(pointKey: String?, params: List>?) {
419 | BuryPointContext.buryPointUploader?.upload(pointKey, params)
420 | }
421 | }
422 | }
--------------------------------------------------------------------------------