tubinyan 5 лет назад
Сommit
62396aec6d
100 измененных файлов с 4120 добавлено и 0 удалено
  1. 15 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 6 0
      .idea/compiler.xml
  4. 22 0
      .idea/gradle.xml
  5. 30 0
      .idea/jarRepositories.xml
  6. 9 0
      .idea/misc.xml
  7. 1 0
      app/.gitignore
  8. BIN
      app/MyApplicationKey.jks
  9. 35 0
      app/agconnect-services.json
  10. 72 0
      app/build.gradle
  11. 31 0
      app/proguard-rules.pro
  12. 26 0
      app/src/androidTest/java/cn/seecoder/courselearningdemo/ExampleInstrumentedTest.java
  13. 50 0
      app/src/main/AndroidManifest.xml
  14. 34 0
      app/src/main/java/cn/seecoder/courselearningdemo/CourseLearningDemoApplication.java
  15. 38 0
      app/src/main/java/cn/seecoder/courselearningdemo/dao/UserDao.java
  16. 106 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/Course.java
  17. 75 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/CourseOrder.java
  18. 115 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/CourseWare.java
  19. 60 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/PageInfo.java
  20. 48 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/RechargeOrder.java
  21. 34 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/ResultVO.java
  22. 50 0
      app/src/main/java/cn/seecoder/courselearningdemo/model/User.java
  23. 22 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/CourseOrderService.java
  24. 21 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/CourseService.java
  25. 14 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/CourseWareService.java
  26. 11 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/NotifyService.java
  27. 17 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/RetrofitClient.java
  28. 22 0
      app/src/main/java/cn/seecoder/courselearningdemo/network/UserService.java
  29. 183 0
      app/src/main/java/cn/seecoder/courselearningdemo/service/MyPushService.java
  30. 101 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/activity/CourseActivity.java
  31. 201 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/activity/MainActivity.java
  32. 121 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/adapter/CourseAdapter.java
  33. 78 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/adapter/CourseWareAdapter.java
  34. 251 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/AccountFragment.java
  35. 61 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/BaseFragment.java
  36. 207 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/HomeFragment.java
  37. 128 0
      app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/StudyFragment.java
  38. 65 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/CipherUtil.java
  39. 26 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/Constant.java
  40. 19 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/DateUtil.java
  41. 20 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/KeyboardUtil.java
  42. 5 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/LoadState.java
  43. 51 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/LogUtil.java
  44. 5 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/RechargeState.java
  45. 11 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/SizeUtil.java
  46. 7 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/StringUtil.java
  47. 26 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/ToastUtil.java
  48. 16 0
      app/src/main/java/cn/seecoder/courselearningdemo/utils/UrlUtil.java
  49. 89 0
      app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/CourseViewModel.java
  50. 150 0
      app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/HomeViewModel.java
  51. 292 0
      app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/RechargeViewModel.java
  52. 179 0
      app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/SharedViewModel.java
  53. 55 0
      app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/StudyViewModel.java
  54. 30 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  55. BIN
      app/src/main/res/drawable/account_null.jpg
  56. 8 0
      app/src/main/res/drawable/bg_blue_circle.xml
  57. 9 0
      app/src/main/res/drawable/bg_blue_circle_side.xml
  58. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  59. BIN
      app/src/main/res/drawable/ic_push_notification.png
  60. 6 0
      app/src/main/res/drawable/navigation_bar_color.xml
  61. 9 0
      app/src/main/res/drawable/shape_edit_box_bg.xml
  62. 71 0
      app/src/main/res/layout/activity_course.xml
  63. 25 0
      app/src/main/res/layout/activity_main.xml
  64. 172 0
      app/src/main/res/layout/fragment_account.xml
  65. 57 0
      app/src/main/res/layout/fragment_home.xml
  66. 67 0
      app/src/main/res/layout/fragment_study.xml
  67. 73 0
      app/src/main/res/layout/item_course_content.xml
  68. 35 0
      app/src/main/res/layout/item_course_ware_content.xml
  69. 15 0
      app/src/main/res/menu/my_navigation_items.xml
  70. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  71. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  72. BIN
      app/src/main/res/mipmap-hdpi/course_checked.png
  73. BIN
      app/src/main/res/mipmap-hdpi/course_normal.png
  74. BIN
      app/src/main/res/mipmap-hdpi/course_press.png
  75. BIN
      app/src/main/res/mipmap-hdpi/diamond_medium.png
  76. BIN
      app/src/main/res/mipmap-hdpi/diamond_small.png
  77. BIN
      app/src/main/res/mipmap-hdpi/home_checked.png
  78. BIN
      app/src/main/res/mipmap-hdpi/home_normal.png
  79. BIN
      app/src/main/res/mipmap-hdpi/home_press.png
  80. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  81. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  82. BIN
      app/src/main/res/mipmap-hdpi/lock.png
  83. BIN
      app/src/main/res/mipmap-hdpi/logo_180x180.png
  84. BIN
      app/src/main/res/mipmap-hdpi/search_checked.png
  85. BIN
      app/src/main/res/mipmap-hdpi/search_normal.png
  86. BIN
      app/src/main/res/mipmap-hdpi/search_press.png
  87. BIN
      app/src/main/res/mipmap-hdpi/user_checked.png
  88. BIN
      app/src/main/res/mipmap-hdpi/user_normal.png
  89. BIN
      app/src/main/res/mipmap-hdpi/user_press.png
  90. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  91. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  92. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  93. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  94. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  95. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  96. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  97. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  98. 16 0
      app/src/main/res/values-night/themes.xml
  99. 27 0
      app/src/main/res/values/colors.xml
  100. 6 0
      app/src/main/res/values/dimens.xml

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 6 - 0
.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="1.8" />
+  </component>
+</project>

+ 22 - 0
.idea/gradle.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="testRunner" value="PLATFORM" />
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleJvm" value="1.8 (2)" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/app" />
+          </set>
+        </option>
+        <option name="resolveModulePerSourceSet" value="false" />
+        <option name="useQualifiedModuleNames" value="true" />
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 30 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="BintrayJCenter" />
+      <option name="name" value="BintrayJCenter" />
+      <option name="url" value="https://jcenter.bintray.com/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="Google" />
+      <option name="name" value="Google" />
+      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="maven" />
+      <option name="name" value="maven" />
+      <option name="url" value="https://developer.huawei.com/repo/" />
+    </remote-repository>
+  </component>
+</project>

+ 9 - 0
.idea/misc.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

BIN
app/MyApplicationKey.jks


+ 35 - 0
app/agconnect-services.json

@@ -0,0 +1,35 @@
+{
+	"agcgw":{
+		"backurl":"connect-drcn.dbankcloud.cn",
+		"url":"connect-drcn.hispace.hicloud.com"
+	},
+	"client":{
+		"appType":"1",
+		"cp_id":"290086000122119912",
+		"product_id":"736430079244745901",
+		"client_id":"519445263740306624",
+		"client_secret":"B11E85F0E4747BC0520F1BDE2E04B89CD3241E0624C6D2DCBDF4DEFA61D87F0C",
+		"project_id":"736430079244745901",
+		"app_id":"103511055",
+		"api_key":"CgB6e3x9zeChUnab/1T6u0U41r3rMx+x7YuwvqYxIl5iuSd6mxSnhZtE1fgBjpk1jKxyvn2EIBuIhyi3L0JfvILC",
+		"package_name":"cn.seecoder.courselearningdemo"
+	},
+	"service":{
+		"analytics":{
+			"collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
+			"resource_id":"p1",
+			"channel_id":""
+		},
+		"search":{
+			"url":"https://search-drcn.cloud.huawei.com"
+		},
+		"cloudstorage":{
+			"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn"
+		},
+		"ml":{
+			"mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn"
+		}
+	},
+	"region":"CN",
+	"configuration_version":"1.0"
+}

+ 72 - 0
app/build.gradle

@@ -0,0 +1,72 @@
+plugins {
+    id 'com.android.application'
+    id 'com.huawei.agconnect'
+}
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        applicationId "cn.seecoder.courselearningdemo"
+        minSdkVersion 29
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    signingConfigs {
+        config {
+            keyAlias 'HMS_demo_key'
+            keyPassword 'seecoder'
+            storeFile file('MyApplicationKey.jks')
+            storePassword 'seecoder'
+            v1SigningEnabled true
+            v2SigningEnabled true
+        }
+    }
+
+    buildTypes {
+        debug {
+            signingConfig signingConfigs.config
+            debuggable true
+        }
+        release {
+            signingConfig signingConfigs.config
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation 'com.huawei.hms:hwid:5.0.5.301'
+    implementation 'com.huawei.hms:iap:5.0.1.300'
+    implementation 'com.huawei.hms:push:5.0.2.300'
+    implementation 'androidx.recyclerview:recyclerview:1.1.0'
+    implementation 'com.squareup.retrofit2:retrofit:2.7.0'
+    implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
+    // 生成圆形的头像图片
+    implementation 'de.hdodenhof:circleimageview:3.1.0'
+    // 用于加载图片的框架
+    implementation 'com.github.bumptech.glide:glide:4.11.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+    // 用于刷新列表的框架
+    implementation 'com.lcodecorex:tkrefreshlayout:1.0.7'
+    // View注入框架,不用再写findViewById
+    implementation 'com.jakewharton:butterknife:10.2.1'
+    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    implementation 'com.google.android.material:material:1.2.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+    testImplementation 'junit:junit:4.+'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}

+ 31 - 0
app/proguard-rules.pro

@@ -0,0 +1,31 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-ignorewarnings
+-keepattributes *Annotation*
+-keepattributes Exceptions
+-keepattributes InnerClasses
+-keepattributes Signature
+-keepattributes SourceFile,LineNumberTable
+-keep class com.hianalytics.android.**{*;}
+-keep class com.huawei.updatesdk.**{*;}
+-keep class com.huawei.hms.**{*;}

+ 26 - 0
app/src/androidTest/java/cn/seecoder/courselearningdemo/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package cn.seecoder.courselearningdemo;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("cn.seecoder.courselearningdemo", appContext.getPackageName());
+    }
+}

+ 50 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="cn.seecoder.courselearningdemo">
+    <!--check Internet-->
+    <uses-permission android:name="android.permission.INTERNET" />
+    <!--check network permissions-->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <!--check wifi state-->
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <!--check phone state-->
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <!--if your app is running above android 8.0 and use "targetSdkVersion>=26, need add permission -->
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+    <application
+        android:name=".CourseLearningDemoApplication"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".ui.activity.MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:name=".ui.activity.CourseActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data
+                    android:host="cn.seecoder.courselearning"
+                    android:path="/course"
+                    android:scheme="pushscheme" />
+            </intent-filter>
+        </activity>
+        <service
+            android:name=".service.MyPushService"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="com.huawei.push.action.MESSAGING_EVENT" />
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>

+ 34 - 0
app/src/main/java/cn/seecoder/courselearningdemo/CourseLearningDemoApplication.java

@@ -0,0 +1,34 @@
+package cn.seecoder.courselearningdemo;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.content.Context;
+
+/**
+ * CourseLearningDemoApplication类 用于全局获取Context、读取配置的ApiKey
+ */
+public class CourseLearningDemoApplication extends Application {
+    @SuppressLint("StaticFieldLeak")
+    private static Context context;
+    private static String appId;
+    private static String apiKey;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        context = getApplicationContext();
+        apiKey = "CgB6e3x9zeChUnab/1T6u0U41r3rMx+x7YuwvqYxIl5iuSd6mxSnhZtE1fgBjpk1jKxyvn2EIBuIhyi3L0JfvILC";
+    }
+
+    public static Context getContext(){
+        return context;
+    }
+
+    public static String getAppId() {
+        return appId;
+    }
+
+    public static String getApiKey() {
+        return apiKey;
+    }
+}

+ 38 - 0
app/src/main/java/cn/seecoder/courselearningdemo/dao/UserDao.java

@@ -0,0 +1,38 @@
+package cn.seecoder.courselearningdemo.dao;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.google.gson.Gson;
+
+import cn.seecoder.courselearningdemo.CourseLearningDemoApplication;
+import cn.seecoder.courselearningdemo.model.User;
+
+/**
+ * 使用SharedPreferences 实现本地数据库
+ */
+public class UserDao {
+
+    private static final SharedPreferences sharedPreferences = CourseLearningDemoApplication.getContext().getSharedPreferences("course_learning", Context.MODE_PRIVATE);
+
+    public static void saveUser(User user){
+        SharedPreferences.Editor editor = sharedPreferences.edit();
+        editor.putString("user", new Gson().toJson(user));
+        editor.apply();
+    }
+
+    public static void clearUser(){
+        SharedPreferences.Editor editor = sharedPreferences.edit();
+        editor.remove("user");
+        editor.apply();
+    }
+
+    public static User getUser(){
+        String userJson = sharedPreferences.getString("user", "");
+        return new Gson().fromJson(userJson, User.class);
+    }
+
+    public static boolean isUserSaved(){
+        return sharedPreferences.contains("user");
+    }
+}

+ 106 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/Course.java

@@ -0,0 +1,106 @@
+package cn.seecoder.courselearningdemo.model;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 实现序列化接口,使得Course类的实例对象可以在Activity之间传递
+ */
+public class Course implements Serializable {
+    private Integer id;
+    private String name;
+    private String type;
+    private String intro;
+    private String picture;
+    private String provider;
+    private Integer cost;
+    private boolean bought;
+    private Date createTime;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getIntro() {
+        return intro;
+    }
+
+    public void setIntro(String intro) {
+        this.intro = intro;
+    }
+
+    public String getPicture() {
+        return picture;
+    }
+
+    public void setPicture(String picture) {
+        this.picture = picture;
+    }
+
+    public String getProvider() {
+        return provider;
+    }
+
+    public void setProvider(String provider) {
+        this.provider = provider;
+    }
+
+    public Integer getCost() {
+        return cost;
+    }
+
+    public void setCost(Integer cost) {
+        this.cost = cost;
+    }
+
+    public boolean isBought() {
+        return bought;
+    }
+
+    public void setBought(boolean bought) {
+        this.bought = bought;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    @Override
+    public String toString() {
+        return "Course{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                ", type='" + type + '\'' +
+                ", intro='" + intro + '\'' +
+                ", picture='" + picture + '\'' +
+                ", provider='" + provider + '\'' +
+                ", cost=" + cost +
+                ", bought=" + bought +
+                ", createTime=" + createTime +
+                '}';
+    }
+}

+ 75 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/CourseOrder.java

@@ -0,0 +1,75 @@
+package cn.seecoder.courselearningdemo.model;
+
+import java.util.Date;
+
+public class CourseOrder {
+    private Integer id;
+
+    private Integer cost;
+
+    private Integer courseId;
+
+    private String courseName;
+
+    private Date createTime;
+
+    private String userId;
+
+    private Integer status;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getCost() {
+        return cost;
+    }
+
+    public void setCost(Integer cost) {
+        this.cost = cost;
+    }
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public String getCourseName() {
+        return courseName;
+    }
+
+    public void setCourseName(String courseName) {
+        this.courseName = courseName;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+}

+ 115 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/CourseWare.java

@@ -0,0 +1,115 @@
+package cn.seecoder.courselearningdemo.model;
+
+import java.util.Date;
+
+public class CourseWare {
+    private Integer id;
+
+    private Integer courseId;
+
+    private Integer number;
+
+    private String title;
+
+    private String fileName;
+
+    private String fileType;
+
+    private String fileSize;
+
+    private Boolean freeFlag;
+
+    private Boolean downloadFlag;
+
+    private Boolean availableFlag;
+
+    private Date uploadTime;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public Integer getNumber() {
+        return number;
+    }
+
+    public void setNumber(Integer number) {
+        this.number = number;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getFileType() {
+        return fileType;
+    }
+
+    public void setFileType(String fileType) {
+        this.fileType = fileType;
+    }
+
+    public String getFileSize() {
+        return fileSize;
+    }
+
+    public void setFileSize(String fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    public Boolean getFreeFlag() {
+        return freeFlag;
+    }
+
+    public void setFreeFlag(Boolean freeFlag) {
+        this.freeFlag = freeFlag;
+    }
+
+    public Boolean getDownloadFlag() {
+        return downloadFlag;
+    }
+
+    public void setDownloadFlag(Boolean downloadFlag) {
+        this.downloadFlag = downloadFlag;
+    }
+
+    public Boolean getAvailableFlag() {
+        return availableFlag;
+    }
+
+    public void setAvailableFlag(Boolean availableFlag) {
+        this.availableFlag = availableFlag;
+    }
+
+    public Date getUploadTime() {
+        return uploadTime;
+    }
+
+    public void setUploadTime(Date uploadTime) {
+        this.uploadTime = uploadTime;
+    }
+}

+ 60 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/PageInfo.java

@@ -0,0 +1,60 @@
+package cn.seecoder.courselearningdemo.model;
+
+import java.util.List;
+
+public class PageInfo<T> {
+    private int pageNum;
+    private int pageSize;
+    private int size;
+    private long total;
+    private int pages;
+    private List<T> list;
+
+    public int getPageNum() {
+        return pageNum;
+    }
+
+    public void setPageNum(int pageNum) {
+        this.pageNum = pageNum;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public void setSize(int size) {
+        this.size = size;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long total) {
+        this.total = total;
+    }
+
+    public int getPages() {
+        return pages;
+    }
+
+    public void setPages(int pages) {
+        this.pages = pages;
+    }
+
+    public List<T> getList() {
+        return list;
+    }
+
+    public void setList(List<T> list) {
+        this.list = list;
+    }
+}

+ 48 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/RechargeOrder.java

@@ -0,0 +1,48 @@
+package cn.seecoder.courselearningdemo.model;
+
+import java.util.Date;
+
+public class RechargeOrder {
+    private String orderId;
+
+    private String uid;
+
+    private Integer value;
+
+    private Date createTime;
+
+    public String getOrderId() {
+        return orderId;
+    }
+
+    public void setOrderId(String orderId) {
+        this.orderId = orderId == null ? null : orderId.trim();
+    }
+
+    public String getUid() {
+        return uid;
+    }
+
+    public void setUid(String uid) {
+        this.uid = uid == null ? null : uid.trim();
+    }
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public void setValue(Integer value) {
+        this.value = value;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public RechargeOrder() {
+    }
+}

+ 34 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/ResultVO.java

@@ -0,0 +1,34 @@
+package cn.seecoder.courselearningdemo.model;
+
+public class ResultVO<T> {
+
+    private Integer code;
+
+    private String msg;
+
+    private T data;
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+}

+ 50 - 0
app/src/main/java/cn/seecoder/courselearningdemo/model/User.java

@@ -0,0 +1,50 @@
+package cn.seecoder.courselearningdemo.model;
+
+public class User {
+    private String uid;
+
+    private String uname;
+
+    private String picture;
+
+    private Integer balance;
+
+    public String getUid() {
+        return uid;
+    }
+
+    public void setUid(String uid) {
+        this.uid = uid;
+    }
+
+    public String getUname() {
+        return uname;
+    }
+
+    public void setUname(String uname) {
+        this.uname = uname;
+    }
+
+    public String getPicture() {
+        return picture;
+    }
+
+    public void setPicture(String picture) {
+        this.picture = picture;
+    }
+
+    public Integer getBalance() {
+        return balance;
+    }
+
+    public void setBalance(Integer balance) {
+        this.balance = balance;
+    }
+
+    public String toString(){
+        return "uid: " + uid + "\r\n"
+                + "uname: " + uname + "\r\n"
+                + "picture: " + picture + "\r\n"
+                + "balance: " + balance + "\r\n";
+    }
+}

+ 22 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/CourseOrderService.java

@@ -0,0 +1,22 @@
+package cn.seecoder.courselearningdemo.network;
+
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.CourseOrder;
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+
+public interface CourseOrderService {
+    @POST("/course_order/create")
+    Call<ResultVO<CourseOrder>> createCourseOrder(@Body CourseOrder courseOrder);
+
+    /**
+     * 根据uid查询用户所有的订单
+     */
+    @GET("/course_order/uid/{uid}")
+    Call<List<CourseOrder>> getCourseOrders(@Path(value = "uid") String uid);
+}

+ 21 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/CourseService.java

@@ -0,0 +1,21 @@
+package cn.seecoder.courselearningdemo.network;
+
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.model.PageInfo;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface CourseService {
+    @GET("/course/{courseId}")
+    Call<Course> getCourse(@Path(value = "courseId") Integer courseId, @Query(value = "uid")String uid);
+    @GET("/course/all/{page}")
+    Call<PageInfo<Course>> getAllCourses(@Path(value = "page") Integer page, @Query(value = "uid")String uid, @Query(value = "key")String key);
+    @GET("/course/by_type/{page}")
+    Call<PageInfo<Course>> getCoursesByType(@Path(value = "page") Integer page, @Query(value = "uid")String uid, @Query(value = "type")String type);
+    @GET("/course/uid/{uid}")
+    Call<List<Course>> getCourseByUid(@Path(value = "uid") String uid);
+}

+ 14 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/CourseWareService.java

@@ -0,0 +1,14 @@
+package cn.seecoder.courselearningdemo.network;
+
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.CourseWare;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface CourseWareService {
+    @GET("/course_ware/course/{courseId}")
+    Call<List<CourseWare>> getCourseWare(@Path(value = "courseId") Integer courseId, @Query(value = "uid") String uid);
+}

+ 11 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/NotifyService.java

@@ -0,0 +1,11 @@
+package cn.seecoder.courselearningdemo.network;
+
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import retrofit2.Call;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+public interface NotifyService {
+    @POST("/notify/course_order_success")
+    Call<ResultVO<String>> sendCourseOrderSuccessNotify(@Query(value = "uid") String uid, @Query(value = "courseId") Integer courseId);
+}

+ 17 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/RetrofitClient.java

@@ -0,0 +1,17 @@
+package cn.seecoder.courselearningdemo.network;
+
+import cn.seecoder.courselearningdemo.utils.Constant;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+public class RetrofitClient {
+    private static final Retrofit retrofit = new Retrofit.Builder()
+            .baseUrl(Constant.HOST) //设置网络请求的Url地址
+            .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器
+            .build();
+    public static CourseService courseService = retrofit.create(CourseService.class);
+    public static CourseWareService courseWareService = retrofit.create(CourseWareService.class);
+    public static CourseOrderService courseOrderService = retrofit.create(CourseOrderService.class);
+    public static UserService userService = retrofit.create(UserService.class);
+    public static NotifyService notifyService = retrofit.create(NotifyService.class);
+}

+ 22 - 0
app/src/main/java/cn/seecoder/courselearningdemo/network/UserService.java

@@ -0,0 +1,22 @@
+package cn.seecoder.courselearningdemo.network;
+
+import cn.seecoder.courselearningdemo.model.RechargeOrder;
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import cn.seecoder.courselearningdemo.model.User;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface UserService {
+    @GET("/user/{uid}")
+    Call<User> getUser(@Path(value = "uid" )String uid);
+    @POST("/user/login")
+    Call<User> login(@Body User user);
+    @POST("/user/recharge")
+    Call<ResultVO<User>> recharge(@Body RechargeOrder rechargeOrder);
+    @POST("/user_token/refresh")
+    Call<ResultVO<String>> refreshUserToken(@Query(value = "uid") String uid, @Query(value = "token") String token);
+}

+ 183 - 0
app/src/main/java/cn/seecoder/courselearningdemo/service/MyPushService.java

@@ -0,0 +1,183 @@
+/*
+ *  Copyright 2020. Huawei TechnoLogUtilies Co., Ltd. All rights reserved.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package cn.seecoder.courselearningdemo.service;
+
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.huawei.hms.push.HmsMessageService;
+import com.huawei.hms.push.RemoteMessage;
+import com.huawei.hms.push.SendException;
+
+import java.util.Arrays;
+
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+
+public class MyPushService extends HmsMessageService {
+    private static final String TAG = "MyPushService";
+    private final static String CODELABS_ACTION = "com.example.codelabpush.action";
+
+    /**
+     * When an app calls the getToken method to apply for a token from the server,
+     * if the server does not return the token during current method calling, the server can return the token through this method later.
+     * This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
+     * @param token token
+     */
+    @Override
+    public void onNewToken(String token) {
+        LogUtil.i(TAG, "received refresh token:" + token);
+        // send the token to your app server.
+        if (!TextUtils.isEmpty(token)) {
+            // This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
+            refreshedTokenToServer(token);
+        }
+
+        Intent intent = new Intent();
+        intent.setAction(CODELABS_ACTION);
+        intent.putExtra("method", "onNewToken");
+        intent.putExtra("msg", "onNewToken called, token: " + token);
+
+        sendBroadcast(intent);
+    }
+    private void refreshedTokenToServer(String token) {
+        LogUtil.i(TAG, "sending token to server. token:" + token);
+    }
+
+    /**
+     * This method is used to receive downstream data messages.
+     * This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
+     *
+     * @param message RemoteMessage
+     */
+    @Override
+    public void onMessageReceived(RemoteMessage message) {
+        System.out.println("onMessageReceived is called");
+        LogUtil.i(TAG, "onMessageReceived is called");
+        if (message == null) {
+            LogUtil.e(TAG, "Received message entity is null!");
+            return;
+        }
+        // getCollapseKey() Obtains the classification identifier (collapse key) of a message.
+        // getData() Obtains valid content data of a message.
+        // getMessageId() Obtains the ID of a message.
+        // getMessageType() Obtains the type of a message.
+        // getNotification() Obtains the notification data instance from a message.
+        // getOriginalUrgency() Obtains the original priority of a message.
+        // getSentTime() Obtains the time when a message is sent from the server.
+        // getTo() Obtains the recipient of a message.
+
+        LogUtil.i(TAG, "getCollapseKey: " + message.getCollapseKey()
+                + "\n getData: " + message.getData()
+                + "\n getFrom: " + message.getFrom()
+                + "\n getTo: " + message.getTo()
+                + "\n getMessageId: " + message.getMessageId()
+                + "\n getOriginalUrgency: " + message.getOriginalUrgency()
+                + "\n getUrgency: " + message.getUrgency()
+                + "\n getSendTime: " + message.getSentTime()
+                + "\n getMessageType: " + message.getMessageType()
+                + "\n getTtl: " + message.getTtl());
+
+        // getBody() Obtains the displayed content of a message
+        // getTitle() Obtains the title of a message
+        // getTitleLocalizationKey() Obtains the key of the displayed title of a notification message
+        // getTitleLocalizationArgs() Obtains variable parameters of the displayed title of a message
+        // getBodyLocalizationKey() Obtains the key of the displayed content of a message
+        // getBodyLocalizationArgs() Obtains variable parameters of the displayed content of a message
+        // getIcon() Obtains icons from a message
+        // getSound() Obtains the sound from a message
+        // getTag() Obtains the tag from a message for message overwriting
+        // getColor() Obtains the colors of icons in a message
+        // getClickAction() Obtains actions triggered by message tapping
+        // getChannelId() Obtains IDs of channels that support the display of messages
+        // getImageUrl() Obtains the image URL from a message
+        // getLink() Obtains the URL to be accessed from a message
+        // getNotifyId() Obtains the unique ID of a message
+
+        RemoteMessage.Notification notification = message.getNotification();
+        if (notification != null) {
+            LogUtil.i(TAG, "\n getImageUrl: " + notification.getImageUrl()
+                    + "\n getTitle: " + notification.getTitle()
+                    + "\n getTitleLocalizationKey: " + notification.getTitleLocalizationKey()
+                    + "\n getTitleLocalizationArgs: " + Arrays.toString(notification.getTitleLocalizationArgs())
+                    + "\n getBody: " + notification.getBody()
+                    + "\n getBodyLocalizationKey: " + notification.getBodyLocalizationKey()
+                    + "\n getBodyLocalizationArgs: " + Arrays.toString(notification.getBodyLocalizationArgs())
+                    + "\n getIcon: " + notification.getIcon()
+                    + "\n getSound: " + notification.getSound()
+                    + "\n getTag: " + notification.getTag()
+                    + "\n getColor: " + notification.getColor()
+                    + "\n getClickAction: " + notification.getClickAction()
+                    + "\n getChannelId: " + notification.getChannelId()
+                    + "\n getLink: " + notification.getLink()
+                    + "\n getNotifyId: " + notification.getNotifyId());
+        }
+
+        Intent intent = new Intent();
+        intent.setAction(CODELABS_ACTION);
+        intent.putExtra("method", "onMessageReceived");
+        intent.putExtra("msg", "onMessageReceived called, message id:" + message.getMessageId() + ", payload data:"
+                + message.getData());
+
+        sendBroadcast(intent);
+
+        Boolean judgeWhetherIn10s = false;
+
+        // If the messages are not processed in 10 seconds, the app needs to use WorkManager for processing.
+        if (judgeWhetherIn10s) {
+            startWorkManagerJob(message);
+        } else {
+            // Process message within 10s
+            processWithin10s(message);
+        }
+    }
+    private void startWorkManagerJob(RemoteMessage message) {
+        LogUtil.d(TAG, "Start new Job processing.");
+    }
+    private void processWithin10s(RemoteMessage message) {
+        LogUtil.d(TAG, "Processing now.");
+    }
+
+    @Override
+    public void onMessageSent(String msgId) {
+        LogUtil.i(TAG, "onMessageSent called, Message id:" + msgId);
+        Intent intent = new Intent();
+        intent.setAction(CODELABS_ACTION);
+        intent.putExtra("method", "onMessageSent");
+        intent.putExtra("msg", "onMessageSent called, Message id:" + msgId);
+
+        sendBroadcast(intent);
+    }
+
+    @Override
+    public void onSendError(String msgId, Exception exception) {
+        LogUtil.i(TAG, "onSendError called, message id:" + msgId + ", ErrCode:"
+                + ((SendException) exception).getErrorCode() + ", description:" + exception.getMessage());
+
+        Intent intent = new Intent();
+        intent.setAction(CODELABS_ACTION);
+        intent.putExtra("method", "onSendError");
+        intent.putExtra("msg", "onSendError called, message id:" + msgId + ", ErrCode:"
+            + ((SendException) exception).getErrorCode() + ", description:" + exception.getMessage());
+
+        sendBroadcast(intent);
+    }
+
+    @Override
+    public void onTokenError(Exception e) {
+        super.onTokenError(e);
+    }
+}

+ 101 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/activity/CourseActivity.java

@@ -0,0 +1,101 @@
+package cn.seecoder.courselearningdemo.ui.activity;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.model.CourseWare;
+import cn.seecoder.courselearningdemo.ui.adapter.CourseWareAdapter;
+import cn.seecoder.courselearningdemo.utils.SizeUtil;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import cn.seecoder.courselearningdemo.viewmodel.CourseViewModel;
+
+public class CourseActivity extends AppCompatActivity {
+    private Integer courseId;
+    private Course course;
+    private CourseWareAdapter mAdapter;
+    private CourseViewModel courseViewModel;
+    @BindView(R.id.course_ware_content_list)
+    public RecyclerView recyclerView;
+    @BindView(R.id.course_content_name_tv)
+    TextView courseNameTv;
+    @BindView(R.id.course_content_provider_tv)
+    TextView courseProviderTv;
+    @BindView(R.id.course_content_intro_tv)
+    TextView courseIntroTv;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_course);
+        ButterKnife.bind(this);
+        if(getIntent().getExtras() != null) {
+            course = (Course) getIntent().getSerializableExtra("course");
+            if (course == null)
+                courseId = getIntent().getIntExtra("courseId", 0);
+        }else{
+            ToastUtil.showLongToast("当前页面加载错误,请稍后重试!");
+        }
+        initView();
+        initViewModel();
+        initObserver();
+    }
+
+    private void initView(){
+        mAdapter = new CourseWareAdapter();
+        RecyclerView.LayoutManager manager = new LinearLayoutManager(CourseActivity.this);
+        recyclerView.setLayoutManager(manager);
+        recyclerView.setAdapter(mAdapter);
+        // 设置四周的分割线
+        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
+            @Override
+            public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+                outRect.top = SizeUtil.dip2px(CourseActivity.this,5.0f);
+                outRect.bottom = SizeUtil.dip2px(CourseActivity.this,5.0f);
+                outRect.left = SizeUtil.dip2px(CourseActivity.this,2.5f);
+                outRect.right = SizeUtil.dip2px(CourseActivity.this,2.5f);
+            }
+        });
+    }
+
+    private void initViewModel() {
+        courseViewModel = new ViewModelProvider(CourseActivity.this).get(CourseViewModel.class);
+        if(course != null)
+            courseViewModel.setCourse(course);
+        else if(courseId != null && courseId != 0)
+            courseViewModel.loadCourse(courseId);
+    }
+
+    private void initObserver() {
+        courseViewModel.getCourseData().observe(this, new Observer<Course>() {
+            @Override
+            public void onChanged(Course course) {
+                if(course!=null){
+                    courseNameTv.setText(course.getName());
+                    courseProviderTv.setText(course.getProvider());
+                    courseIntroTv.setText(course.getIntro());
+                }
+            }
+        });
+        courseViewModel.getCourseWareData().observe(this, new Observer<List<CourseWare>>() {
+            @Override
+            public void onChanged(List<CourseWare> courseWareList) {
+                mAdapter.setData(courseWareList);
+            }
+        });
+    }
+}

+ 201 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/activity/MainActivity.java

@@ -0,0 +1,201 @@
+package cn.seecoder.courselearningdemo.ui.activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.huawei.agconnect.config.AGConnectServicesConfig;
+import com.huawei.hmf.tasks.OnFailureListener;
+import com.huawei.hmf.tasks.Task;
+import com.huawei.hms.aaid.HmsInstanceId;
+import com.huawei.hms.common.ApiException;
+import com.huawei.hms.common.ResolvableApiException;
+import com.huawei.hms.support.account.AccountAuthManager;
+import com.huawei.hms.support.account.result.AuthAccount;
+import com.huawei.hms.support.account.service.AccountAuthService;
+
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.ui.fragment.AccountFragment;
+import cn.seecoder.courselearningdemo.ui.fragment.BaseFragment;
+import cn.seecoder.courselearningdemo.ui.fragment.HomeFragment;
+import cn.seecoder.courselearningdemo.ui.fragment.StudyFragment;
+import cn.seecoder.courselearningdemo.utils.Constant;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.viewmodel.SharedViewModel;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class MainActivity extends AppCompatActivity {
+    // 导航栏
+    private BottomNavigationView mainNavigationView;
+    // 上一次显示的fragment
+    private BaseFragment lastOneFragment = null;
+    // fragment管理器
+    private FragmentManager fragmentManager = null;
+    private HomeFragment homeFragment;
+    private AccountFragment accountFragment;
+    private StudyFragment studyFragment;
+    private SharedViewModel sharedViewModel;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        initView();
+        initListener();
+        initViewModel();
+        initObserver();
+    }
+
+    private void initView() {
+        mainNavigationView = this.findViewById(R.id.main_navigation_bar);
+        homeFragment = new HomeFragment();
+        studyFragment = new StudyFragment();
+        accountFragment = new AccountFragment();
+        fragmentManager = getSupportFragmentManager();
+        switchFragment(homeFragment);
+    }
+
+    private void initListener(){
+        mainNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
+            @Override
+            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+                switch (item.getItemId()){
+                    case R.id.home:
+                        switchFragment(homeFragment);
+                        break;
+                    case R.id.study:
+                        switchFragment(studyFragment);
+                        break;
+                    case R.id.account:
+                        switchFragment(accountFragment);
+                        break;
+                }
+                return true;
+            }
+        });
+    }
+
+    private void initViewModel() {
+        sharedViewModel = new ViewModelProvider(MainActivity.this).get(SharedViewModel.class);
+    }
+
+    private void initObserver() {
+        sharedViewModel.getUid().observe(this, new Observer<String>() {
+            @Override
+            public void onChanged(String uid) {
+                if(uid != null) {
+                    sharedViewModel.userLoginToServer();
+                    getToken();
+                }
+            }
+        });
+    }
+
+    private void switchFragment(BaseFragment targetFragment) {
+        // 如果上一个fragment跟当前要切换的fragment是同一个,那么不需要切换
+        if(lastOneFragment == targetFragment) {
+            return;
+        }
+        // 使用add和hide的方式来控制Fragment的切换
+        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+        if(lastOneFragment != null) {
+            fragmentTransaction.hide(lastOneFragment);
+        }
+        if(!targetFragment.isAdded()) {
+            fragmentTransaction.add(R.id.main_page_container, targetFragment);
+        } else {
+            fragmentTransaction.show(targetFragment);
+        }
+        lastOneFragment = targetFragment;
+        //fragmentTransaction.replace(R.id.main_page_container, targetFragment);
+        fragmentTransaction.commit();
+    }
+
+    // 处理用户的登录请求
+    public void signIn(){
+        // 获取用于登录的AccountAuthManager
+        AccountAuthService mAuthManager = sharedViewModel.getAccountAuthServiceForSignIn();
+        // 调用getSignInIntent方法并拉起帐号登录授权页面
+        startActivityForResult(mAuthManager.getSignInIntent(), Constant.REQUEST_CODE_SIGN_IN);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        LogUtil.i(this, "onActivityResult  resultCode: "+ requestCode);
+        if (requestCode == Constant.REQUEST_CODE_SIGN_IN) {
+            // 华为账号授权登录结果处理,从AuthAccount中获取Authorization Code
+            Task<AuthAccount> authAccountTask = AccountAuthManager.parseAuthResultFromIntent(data);
+            if (authAccountTask.isSuccessful()) {
+                //登录成功,获取用户的帐号信息和Authorization Code
+                AuthAccount authAccount = authAccountTask.getResult();
+                if (authAccount.getAvatarUri() != null) {
+                    User user = new User();
+                    user.setUid(authAccount.getUnionId());
+                    user.setUname(authAccount.getDisplayName());
+                    user.setPicture(authAccount.getAvatarUriString());
+                    sharedViewModel.updateUserData(user);
+                }
+                LogUtil.i(authAccountTask, "Sign in successfully, server AuthCode: " + authAccount.getAuthorizationCode());
+            } else {
+                //登录失败
+                LogUtil.e(authAccountTask, "Sign in failed, status code: " + ((ApiException) authAccountTask.getException()).getStatusCode());
+            }
+        } else if(requestCode == Constant.REQUEST_CODE_BUY){
+            // 若检查到为华为应用内支付的请求代码,则转交给accountFragment处理
+            accountFragment.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    private void getToken() {
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    // read from agconnect-services.json
+                    String appId = AGConnectServicesConfig.fromContext(MainActivity.this).getString("client/app_id");
+                    String token = HmsInstanceId.getInstance(MainActivity.this).getToken(appId, "HCM");
+                    LogUtil.i("MainActivity", "get token:" + token);
+                    if(!TextUtils.isEmpty(token)) {
+                        LogUtil.i("getToken ", token);
+                        sendRegTokenToServer(token);
+                    }
+                } catch (ApiException e) {
+                    Log.e("MainActivity", "get token failed, " + e);
+                }
+            }
+        }.start();
+    }
+
+    private void sendRegTokenToServer(String token){
+        Call<ResultVO<String>> refreshUserTokenCall = RetrofitClient.userService.refreshUserToken(sharedViewModel.getUid().getValue(), token);
+        refreshUserTokenCall.enqueue(new Callback<ResultVO<String>>() {
+            @Override
+            public void onResponse(Call call, Response response) {
+                LogUtil.i("refreshUserToken", "success");
+            }
+
+            @Override
+            public void onFailure(Call call, Throwable t) {
+                LogUtil.i("refreshUserToken", "fail");
+            }
+        });
+    }
+}

+ 121 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/adapter/CourseAdapter.java

@@ -0,0 +1,121 @@
+package cn.seecoder.courselearningdemo.ui.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import java.util.ArrayList;
+import java.util.List;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import cn.seecoder.courselearningdemo.utils.UrlUtil;
+
+public class CourseAdapter extends RecyclerView.Adapter<CourseAdapter.InnerHolder>{
+    private List<Course> mData = new ArrayList<>();
+    private BuyCourseItemClickListener buyCourseItemClickListener;
+    private ShowCourseItemClickListener showCourseItemClickListener;
+
+    @NonNull
+    @Override
+    public InnerHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_course_content, parent,false);
+        return new InnerHolder(itemView);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull InnerHolder holder, int position) {
+        // 绑定数据
+        Course course = mData.get(position);
+        holder.setData(course);
+        // 若用户已经购买过该课程,则隐藏购买按钮
+        if(course.isBought()) {
+            holder.buyBtn.setVisibility(View.GONE);
+        }else{
+            holder.buyBtn.setVisibility(View.VISIBLE);
+        }
+        holder.buyBtn.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if(buyCourseItemClickListener != null){
+                    buyCourseItemClickListener.buyCourseItemClicked(course);
+                }
+            }
+        });
+        holder.showBtn.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if(showCourseItemClickListener != null){
+                    showCourseItemClickListener.showCourseItemClicked(course);
+                }
+            }
+        });
+    }
+
+    @Override
+    public int getItemCount() {
+        return mData.size();
+    }
+
+    public void setData(List<Course> dataList) {
+        this.mData.clear();
+        this.mData.addAll(dataList);
+        notifyDataSetChanged();
+    }
+
+    public class InnerHolder extends RecyclerView.ViewHolder {
+
+        @BindView(R.id.course_item_picture)
+        protected ImageView cover;
+
+        @BindView(R.id.course_item_name_tv)
+        protected TextView nameTv;
+
+        @BindView(R.id.course_item_intro_tv)
+        protected TextView introTv;
+
+        @BindView(R.id.course_item_buy_btn)
+        protected Button buyBtn;
+
+        @BindView(R.id.course_item_show_btn)
+        protected Button showBtn;
+
+        public InnerHolder(@NonNull View itemView) {
+            super(itemView);
+            ButterKnife.bind(this,itemView);
+        }
+
+        public void setData(Course data) {
+            String coverPath = UrlUtil.getCourseImageUrl(data.getPicture());
+            Glide.with(cover.getContext()).load(coverPath).into(cover);
+            nameTv.setText(data.getName());
+            introTv.setText(data.getIntro());
+            String buyButtonHint = "购买课程  "+data.getCost();
+            buyBtn.setText(buyButtonHint);
+        }
+    }
+
+    public void setBuyCourseItemClickListener(BuyCourseItemClickListener listener) {
+        this.buyCourseItemClickListener = listener;
+    }
+
+    public interface BuyCourseItemClickListener {
+        void buyCourseItemClicked(Course course);
+    }
+
+    public void setShowCourseItemClickListener(ShowCourseItemClickListener listener) {
+        this.showCourseItemClickListener = listener;
+    }
+
+    public interface ShowCourseItemClickListener {
+        void showCourseItemClicked(Course course);
+    }
+}

+ 78 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/adapter/CourseWareAdapter.java

@@ -0,0 +1,78 @@
+package cn.seecoder.courselearningdemo.ui.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.CourseWare;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+
+public class CourseWareAdapter extends RecyclerView.Adapter<CourseWareAdapter.InnerHolder>{
+    private List<CourseWare> mData = new ArrayList<>();
+
+    @NonNull
+    @Override
+    public InnerHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_course_ware_content, parent, false);
+        return new InnerHolder(itemView);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull InnerHolder holder, int position) {
+        // 绑定数据
+        CourseWare courseWare = mData.get(position);
+        holder.setData(courseWare);
+        // 若用户有权限获取课件,则隐藏加锁图标
+        if(courseWare.getAvailableFlag())
+            holder.imageView.setVisibility(View.GONE);
+        holder.itemView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if(courseWare.getAvailableFlag())
+                    // 模拟课件预览效果
+                    ToastUtil.showShortToast("正在预览课件");
+                else
+                    ToastUtil.showShortToast("当前课件尚未解锁");
+
+            }
+        });
+    }
+
+    @Override
+    public int getItemCount() {
+        return mData.size();
+    }
+
+    public void setData(List<CourseWare> dataList) {
+        this.mData.clear();
+        this.mData.addAll(dataList);
+        notifyDataSetChanged();
+    }
+
+    public class InnerHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.course_ware_item_name_tv)
+        protected TextView nameTv;
+        @BindView(R.id.course_ware_locked)
+        protected ImageView imageView;
+
+        public InnerHolder(@NonNull View itemView) {
+            super(itemView);
+            ButterKnife.bind(this,itemView);
+        }
+
+        public void setData(CourseWare data) {
+            nameTv.setText(data.getTitle());
+        }
+    }
+}

+ 251 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/AccountFragment.java

@@ -0,0 +1,251 @@
+package cn.seecoder.courselearningdemo.ui.fragment;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.constraintlayout.widget.Group;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.bumptech.glide.Glide;
+import com.huawei.hms.iap.Iap;
+import com.huawei.hms.iap.entity.InAppPurchaseData;
+import com.huawei.hms.iap.entity.OrderStatusCode;
+import com.huawei.hms.iap.entity.ProductInfo;
+import com.huawei.hms.iap.entity.PurchaseResultInfo;
+import com.huawei.hms.support.api.client.Status;
+
+import org.json.JSONException;
+
+import butterknife.BindView;
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.ui.activity.MainActivity;
+import cn.seecoder.courselearningdemo.utils.CipherUtil;
+import cn.seecoder.courselearningdemo.utils.Constant;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.utils.RechargeState;
+import cn.seecoder.courselearningdemo.utils.StringUtil;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import cn.seecoder.courselearningdemo.viewmodel.RechargeViewModel;
+import cn.seecoder.courselearningdemo.viewmodel.SharedViewModel;
+import de.hdodenhof.circleimageview.CircleImageView;
+
+public class AccountFragment extends BaseFragment {
+    private SharedViewModel sharedViewModel;
+    private RechargeViewModel rechargeViewModel;
+    @BindView(R.id.user_profile)
+    protected CircleImageView profileImageView;
+    @BindView(R.id.login_tv)
+    protected TextView loginTv;
+    @BindView(R.id.user_name)
+    protected TextView userNameTv;
+    @BindView(R.id.user_balance)
+    protected TextView userBalanceTv;
+    @BindView(R.id.user_info_group)
+    protected Group userInfoGroup;
+//    @BindView(R.id.account_courses_btn)
+//    protected Button showCoursesButton;
+//    @BindView(R.id.account_orders_btn)
+//    protected Button showOrdersButton;
+    @BindView(R.id.account_recharge_btn)
+    protected Button rechargeButton;
+    @BindView(R.id.account_logout_btn)
+    protected Button logoutButton;
+    private MainActivity mainActivity;
+
+    @Override
+    protected int getRootViewResId() {
+        return R.layout.fragment_account;
+    }
+
+    @Override
+    protected void initView() {
+        initUnLogInView();
+        mainActivity = (MainActivity) getActivity();
+    }
+
+    @Override
+    protected void initViewModel() {
+        sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
+        rechargeViewModel = new ViewModelProvider(this).get(RechargeViewModel.class);
+    }
+
+    @Override
+    protected void initObserver() {
+        sharedViewModel.getUid().observe(this, new Observer<String>() {
+            @Override
+            public void onChanged(String uid) {
+                if(uid != null) {
+                    // 一旦检测到用户登录,就进行初始化:检测支付环境、检测是否有漏单未发货情况
+                    rechargeViewModel.init(Iap.getIapClient(mainActivity), uid);
+                    rechargeViewModel.checkOwedPurchases();
+                }
+            }
+        });
+        sharedViewModel.getUser().observe(this, new Observer<User>() {
+            @Override
+            public void onChanged(User user) {
+                if (!TextUtils.isEmpty(user.getUname())) {
+                    initLoggedInView();
+                    userNameTv.setText(user.getUname());
+                }
+                if(user.getBalance() != null)
+                    // 当调用setText()方法时,如果传入int型是不会被当成内容而是resourceID来使用
+                    userBalanceTv.setText(String.valueOf(user.getBalance()));
+                if (StringUtil.hasText(user.getPicture()))
+                    Glide.with(AccountFragment.this).load(user.getPicture()).into(profileImageView);
+            }
+        });
+        rechargeViewModel.getRechargeStateData().observe(this, new Observer<RechargeState>() {
+            @Override
+            public void onChanged(RechargeState rechargeState) {
+                if(rechargeState.equals(RechargeState.INITIALIZED)){
+                    Status resolutionStatus = rechargeViewModel.getResolutionStatus();
+                    try {
+                        // 打开华为应用内支付的支付页面
+                        resolutionStatus.startResolutionForResult(mainActivity, Constant.REQUEST_CODE_BUY);
+                    } catch (IntentSender.SendIntentException e) {
+                        e.printStackTrace();
+                    }
+                }else if(rechargeState.equals(RechargeState.SUCCEED)){
+                    // 拉取最新的用户信息 - 触发界面显示的余额变化
+                    sharedViewModel.reloadUserDataFromServer();
+                }
+            }
+        });
+    }
+
+    @Override
+    protected void initListener() {
+        loginTv.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mainActivity.signIn();
+            }
+        });
+        rechargeButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                RechargeState state = rechargeViewModel.getRechargeStateData().getValue();
+                ProductInfo productInfo = rechargeViewModel.getProductInfo();
+                if(!rechargeViewModel.isEnvReady()){
+                    if(state.equals(RechargeState.ENV_ERROR))
+                        ToastUtil.showShortToast("网络异常,请稍后重试!");
+                    else if(state.equals(RechargeState.UNSUPPORTED))
+                        ToastUtil.showShortToast("非常抱歉,您当前登录的华为账号所在的服务地不在华为应用内支付服务支持结算的国家或地区中!");
+                    else
+                        ToastUtil.showShortToast("正在检查支付环境...");
+                    rechargeViewModel.queryIsEnvReady(); // 再次尝试检测支付环境
+                }else{
+                    if (productInfo == null){
+                        if(state.equals(RechargeState.PRODUCT_ERROR))
+                            ToastUtil.showShortToast("商品项加载失败,请稍后重试..");
+                        else
+                            ToastUtil.showShortToast("正在加载商品信息..");
+                        rechargeViewModel.queryProduct(); // 再次尝试加载商品信息
+                    } else {
+                        showPurchaseDialog(productInfo);
+                    }
+                }
+            }
+        });
+        logoutButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                sharedViewModel.signOut();
+//                sharedViewModel.cancelAuthorization();
+                initUnLogInView();
+            }
+        });
+    }
+
+    private void initUnLogInView(){
+        loginTv.setVisibility(View.VISIBLE);
+        userInfoGroup.setVisibility(View.GONE);
+        logoutButton.setVisibility(View.GONE);
+        rechargeButton.setVisibility(View.GONE);
+        profileImageView.setImageResource(R.drawable.account_null);
+    }
+
+    private void initLoggedInView(){
+        loginTv.setVisibility(View.GONE);
+        userInfoGroup.setVisibility(View.VISIBLE);
+        logoutButton.setVisibility(View.VISIBLE);
+        rechargeButton.setVisibility(View.VISIBLE);
+    }
+
+    private void showPurchaseDialog(ProductInfo productInfo){
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity());
+        dialogBuilder.setTitle("购买 "+productInfo.getProductName());
+        dialogBuilder.setMessage("确认支付 "+ productInfo.getPrice()+"?");
+        dialogBuilder.setPositiveButton("确定",
+                new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        rechargeViewModel.createPurchaseIntent();
+                    }
+                });
+        dialogBuilder.setNegativeButton("取消",
+                new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        ToastUtil.showShortToast("取消支付");
+                    }
+                });
+        // 显示对话框
+        dialogBuilder.show();
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        LogUtil.i(this, "onActivityResult 开始处理");
+        if (data == null) {
+            LogUtil.e(this, "onActivityResult data is null");
+            return;
+        }
+        if (requestCode == Constant.REQUEST_CODE_BUY) {
+            // 华为应用内支付结果处理
+            // 调用parsePurchaseResultInfoFromIntent方法解析支付结果数据
+            PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(mainActivity).parsePurchaseResultInfoFromIntent(data);
+            LogUtil.i(purchaseResultInfo, "order state code: "+purchaseResultInfo.getReturnCode());
+            switch (purchaseResultInfo.getReturnCode()) {
+                case OrderStatusCode.ORDER_STATE_CANCEL:
+                    // 用户取消
+                    break;
+                case OrderStatusCode.ORDER_STATE_FAILED:
+                case OrderStatusCode.ORDER_PRODUCT_OWNED:
+                    // 当状态为支付失败或产品已订购时,需要检查是否存在未发货商品(消耗型商品可能需要补单)
+                    ToastUtil.showShortToast("上一笔钻石充值订单可能存在漏单情况,正在检查...");
+                    rechargeViewModel.checkOwedPurchases();
+                    break;
+                case OrderStatusCode.ORDER_STATE_SUCCESS:
+                    // 支付成功
+                    String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
+                    String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
+                    // 若验签成功,则进行发货
+                    if(CipherUtil.doCheck(inAppPurchaseData, inAppPurchaseDataSignature)) {
+                        try {
+                            InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
+                            if(inAppPurchaseDataBean.getProductId().equals(Constant.PRODUCT_ID_FOR_10_DIAMONDS)){
+                                rechargeViewModel.recharge10Diamonds(inAppPurchaseDataBean.getOrderID(), inAppPurchaseDataBean.getPurchaseToken());
+                            }
+                        } catch (JSONException e) {
+                            LogUtil.e("checkOwedPurchases", "InAppPurchaseData JSON parse error");
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+}

+ 61 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/BaseFragment.java

@@ -0,0 +1,61 @@
+package cn.seecoder.courselearningdemo.ui.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import butterknife.ButterKnife;
+
+public abstract class BaseFragment extends Fragment {
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View rootView = loadRootView(inflater, container, savedInstanceState);
+        ButterKnife.bind(this, rootView);
+        initView();
+        initViewModel();
+        initObserver();
+        initListener();
+        return rootView;
+    }
+
+    /**
+     * 如果子类需要初始化各种UI组件,覆盖此方法
+     */
+    protected void initView() {
+
+    }
+
+    /**
+     * 如果子类需要去设置监听事件,覆盖此方法
+     */
+    protected void initListener() {
+
+    }
+
+    /**
+     * 如果子类需要初始化ViewModel,覆盖此方法
+     */
+    protected void initViewModel() {
+
+    }
+    /**
+     * 如果子类需要观察数据变化,覆盖此方法
+     */
+    protected void initObserver() {
+
+    }
+
+    protected View loadRootView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
+        int restId = getRootViewResId();
+        return inflater.inflate(restId, container, false);
+    }
+
+    protected abstract int getRootViewResId();
+}

+ 207 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/HomeFragment.java

@@ -0,0 +1,207 @@
+package cn.seecoder.courselearningdemo.ui.fragment;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.lcodecore.tkrefreshlayout.RefreshListenerAdapter;
+import com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout;
+
+import java.util.List;
+
+import butterknife.BindView;
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.ui.activity.CourseActivity;
+import cn.seecoder.courselearningdemo.ui.adapter.CourseAdapter;
+import cn.seecoder.courselearningdemo.utils.KeyboardUtil;
+import cn.seecoder.courselearningdemo.utils.LoadState;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.utils.SizeUtil;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import cn.seecoder.courselearningdemo.viewmodel.HomeViewModel;
+import cn.seecoder.courselearningdemo.viewmodel.SharedViewModel;
+
+public class HomeFragment extends BaseFragment implements CourseAdapter.BuyCourseItemClickListener, CourseAdapter.ShowCourseItemClickListener {
+    private SharedViewModel sharedViewModel;
+    private HomeViewModel homeViewModel;
+    private CourseAdapter mAdapter;
+    @BindView(R.id.course_list_refresh_layout)
+    protected TwinklingRefreshLayout refreshLayout;
+    @BindView(R.id.course_list_rv)
+    public RecyclerView recyclerView;
+    @BindView(R.id.search_course)
+    protected EditText searchEditText;
+
+    @Override
+    protected int getRootViewResId() {
+        return R.layout.fragment_home;
+    }
+
+    @Override
+    protected void initView() {
+        mAdapter = new CourseAdapter();
+        RecyclerView.LayoutManager manager = new LinearLayoutManager(getContext());
+        recyclerView.setLayoutManager(manager);
+        recyclerView.setAdapter(mAdapter);
+        // 设置四周的分割线
+        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
+            @Override
+            public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+                outRect.top = SizeUtil.dip2px(getContext(),5.0f);
+                outRect.bottom = SizeUtil.dip2px(getContext(),5.0f);
+                outRect.left = SizeUtil.dip2px(getContext(),8.0f);
+                outRect.right = SizeUtil.dip2px(getContext(),8.0f);
+            }
+        });
+        // 允许加载更多
+        refreshLayout.setEnableLoadmore(true);
+        // 允许下拉刷新
+        refreshLayout.setEnableRefresh(true);
+        // 开启越界回弹模式
+        refreshLayout.setEnableOverScroll(true);
+    }
+
+    @Override
+    protected void initViewModel() {
+        sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
+        homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
+    }
+
+    @Override
+    protected void initObserver() {
+        sharedViewModel.getUid().observe(this, new Observer<String>() {
+            @Override
+            public void onChanged(String uid) {
+                homeViewModel.setUid(uid);
+            }
+        });
+        homeViewModel.getData().observe(this, new Observer<List<Course>>() {
+            @Override
+            public void onChanged(List<Course> courses) {
+                LogUtil.d(homeViewModel, "courses changed  size = "+ courses.size());
+                mAdapter.setData(courses);
+            }
+        });
+        homeViewModel.getState().observe(this, new Observer<LoadState>() {
+            @Override
+            public void onChanged(LoadState loadState) {
+                LogUtil.d(homeViewModel, "course list load state :"+loadState.toString());
+                switch (loadState){
+                    case SUCCESS:
+                        refreshLayout.finishLoadmore();
+                        refreshLayout.finishRefreshing();
+                        break;
+                    case EMPTY:
+                        ToastUtil.showLongToast("没有更多内容了");
+                        refreshLayout.finishLoadmore();
+                        refreshLayout.finishRefreshing();
+                        break;
+                    case ERROR:
+                        ToastUtil.showLongToast("网络异常,请稍后重试!");
+                        refreshLayout.finishLoadmore();
+                        refreshLayout.finishRefreshing();
+                        break;
+                    case LOADING:
+                        ToastUtil.showLongToast("请耐心等待,内容正在加载");
+                        break;
+                }
+            }
+        });
+        homeViewModel.getCourseOrderFlag().observe(this, new Observer<Boolean>() {
+            @Override
+            public void onChanged(Boolean aBoolean) {
+                if(aBoolean){
+                    ToastUtil.showShortToast("课程购买成功,请前往课程学习页面查看");
+//                    homeViewModel.query();
+                }else{
+                    ToastUtil.showShortToast("购买失败,请检查钻石余额是否充足");
+                }
+                // 不管成功与否,都刷新用户的余额
+                sharedViewModel.reloadUserDataFromServer();
+            }
+        });
+    }
+
+    @Override
+    protected void initListener() {
+        // 设置当点击搜索框后,按下软键盘的搜索按钮后,开始搜索并收起软键盘
+        searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+            @Override
+            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                homeViewModel.setQueryKey(v.getText().toString());
+                homeViewModel.query();
+                KeyboardUtil.hide(getContext(), v);
+                return true;
+            }
+        });
+        refreshLayout.setOnRefreshListener(new RefreshListenerAdapter() {
+            @Override
+            public void onLoadMore(TwinklingRefreshLayout refreshLayout) {
+                // 去加载更多的内容
+                homeViewModel.getNextPageData();
+            }
+
+            @Override
+            public void onRefresh(TwinklingRefreshLayout refreshLayout) {
+                // 刷新当前页面
+                homeViewModel.query();
+            }
+        });
+        // 为课程列表中的每一项内的两个按钮加上监听器
+        mAdapter.setBuyCourseItemClickListener(this);
+        mAdapter.setShowCourseItemClickListener(this);
+    }
+
+    // 点击了购买课程按钮后执行
+    @Override
+    public void buyCourseItemClicked(Course course) {
+        showPurchaseDialog(course);
+    }
+
+    // 点击了查看课程按钮后执行
+    @Override
+    public void showCourseItemClicked(Course course) {
+        Intent intent = new Intent(getActivity(), CourseActivity.class);
+        // 将course对象绑定到intent上,传递给课程详情页面
+        intent.putExtra("course", course);
+        startActivity(intent);
+    }
+
+    private void showPurchaseDialog(Course course){
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity());
+        dialogBuilder.setTitle("购买课程 "+course.getName());
+        dialogBuilder.setMessage("确认支付 "+ course.getCost()+" 个钻石?");
+        dialogBuilder.setPositiveButton("确定",
+                new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        if(homeViewModel.getUid() == null){
+                            ToastUtil.showShortToast("请先登录账号!");
+                        }else {
+                            homeViewModel.createCourseOrder(course);
+                        }
+                    }
+                });
+        dialogBuilder.setNegativeButton("取消",
+                new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        ToastUtil.showShortToast("取消购买");
+                    }
+                });
+        // 显示对话框
+        dialogBuilder.show();
+    }
+}

+ 128 - 0
app/src/main/java/cn/seecoder/courselearningdemo/ui/fragment/StudyFragment.java

@@ -0,0 +1,128 @@
+package cn.seecoder.courselearningdemo.ui.fragment;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+
+import java.util.List;
+
+import butterknife.BindView;
+import cn.seecoder.courselearningdemo.R;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.ui.activity.CourseActivity;
+import cn.seecoder.courselearningdemo.ui.adapter.CourseAdapter;
+import cn.seecoder.courselearningdemo.utils.SizeUtil;
+import cn.seecoder.courselearningdemo.utils.StringUtil;
+import cn.seecoder.courselearningdemo.viewmodel.SharedViewModel;
+import cn.seecoder.courselearningdemo.viewmodel.StudyViewModel;
+
+public class StudyFragment extends BaseFragment implements CourseAdapter.ShowCourseItemClickListener{
+    private SharedViewModel sharedViewModel;
+    private StudyViewModel studyViewModel;
+    private CourseAdapter mAdapter;
+    @BindView(R.id.login_warn_tv)
+    protected TextView loginWarnTv;
+    @BindView(R.id.none_bought_tv)
+    protected TextView noneBoughtTv;
+    @BindView(R.id.account_courses_fresh_btn)
+    protected Button courseFreshButton;
+    @BindView(R.id.my_course_list_rv)
+    protected RecyclerView recyclerView;
+
+    @Override
+    protected int getRootViewResId() {
+        return R.layout.fragment_study;
+    }
+
+    @Override
+    protected void initView() {
+        mAdapter = new CourseAdapter();
+        RecyclerView.LayoutManager manager = new LinearLayoutManager(getContext());
+        recyclerView.setLayoutManager(manager);
+        recyclerView.setAdapter(mAdapter);
+        // 设置四周的分割线
+        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
+            @Override
+            public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+                outRect.top = SizeUtil.dip2px(getContext(),5.0f);
+                outRect.bottom = SizeUtil.dip2px(getContext(),5.0f);
+                outRect.left = SizeUtil.dip2px(getContext(),8.0f);
+                outRect.right = SizeUtil.dip2px(getContext(),8.0f);
+            }
+        });
+    }
+
+    @Override
+    protected void initViewModel() {
+        sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
+        studyViewModel = new ViewModelProvider(this).get(StudyViewModel.class);
+    }
+
+    @Override
+    protected void initObserver() {
+        sharedViewModel.getUid().observe(this, new Observer<String>() {
+            @Override
+            public void onChanged(String uid) {
+               if(StringUtil.hasText(uid)) {
+                   studyViewModel.setUid(uid);
+                   initLoggedInView();
+               }else {
+                   studyViewModel.setUid(null);
+                   initUnLogInView();
+               }
+            }
+        });
+        studyViewModel.getData().observe(this, new Observer<List<Course>>() {
+            @Override
+            public void onChanged(List<Course> courses) {
+                if (courses.size() == 0)
+                    noneBoughtTv.setVisibility(View.VISIBLE);
+                mAdapter.setData(courses);
+            }
+        });
+    }
+
+    @Override
+    protected void initListener() {
+        mAdapter.setShowCourseItemClickListener(this);
+        courseFreshButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                studyViewModel.loadData();
+            }
+        });
+    }
+
+    // 点击了查看课程按钮后执行
+    @Override
+    public void showCourseItemClicked(Course course) {
+        Intent intent = new Intent(getActivity(), CourseActivity.class);
+        // 将course对象绑定到intent上,传递给课程详情页面
+        intent.putExtra("course", course);
+        startActivity(intent);
+    }
+
+    private void initUnLogInView(){
+        loginWarnTv.setVisibility(View.VISIBLE);
+        courseFreshButton.setVisibility(View.GONE);
+        recyclerView.setVisibility(View.GONE);
+        noneBoughtTv.setVisibility(View.GONE);
+    }
+
+    private void initLoggedInView(){
+        courseFreshButton.setVisibility(View.VISIBLE);
+        loginWarnTv.setVisibility(View.GONE);
+        recyclerView.setVisibility(View.VISIBLE);
+    }
+}

+ 65 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/CipherUtil.java

@@ -0,0 +1,65 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import static cn.seecoder.courselearningdemo.utils.Constant.IAP_PUBLIC_KEY;
+
+public class CipherUtil {
+    private static final String TAG = "CipherUtil";
+
+    private static final String SIGN_ALGORITHMS = "SHA256WithRSA";
+
+    private static final String PUBLIC_KEY = IAP_PUBLIC_KEY;
+
+    /**
+     * 校验签名信息
+     * @param content 结果字符串
+     * @param sign 签名字符串
+     * @return 是否校验通过
+     */
+    public static boolean doCheck(String content, String sign) {
+        if (TextUtils.isEmpty(PUBLIC_KEY)) {
+            Log.e(TAG, "publicKey is null");
+            return false;
+        }
+
+        if (TextUtils.isEmpty(content) || TextUtils.isEmpty(sign)) {
+            Log.e(TAG, "data is error");
+            return false;
+        }
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            byte[] encodedKey = Base64.decode(PUBLIC_KEY, Base64.DEFAULT);
+            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
+
+            java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
+
+            signature.initVerify(pubKey);
+            signature.update(content.getBytes(StandardCharsets.UTF_8));
+
+            return signature.verify(Base64.decode(sign, Base64.DEFAULT));
+
+        } catch (NoSuchAlgorithmException e) {
+            Log.e(TAG, "doCheck NoSuchAlgorithmException" + e);
+        } catch (InvalidKeySpecException e) {
+            Log.e(TAG, "doCheck InvalidKeySpecException" + e);
+        } catch (InvalidKeyException e) {
+            Log.e(TAG, "doCheck InvalidKeyException" + e);
+        } catch (SignatureException e) {
+            Log.e(TAG, "doCheck SignatureException" + e);
+        }
+        return false;
+    }
+}

+ 26 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/Constant.java

@@ -0,0 +1,26 @@
+package cn.seecoder.courselearningdemo.utils;
+
+public class Constant {
+    // login code
+    public static final int REQUEST_CODE_SIGN_IN = 8888;
+    // buy code
+    public static final int REQUEST_CODE_BUY = 6666;
+    // server host url
+    public static final String HOST = "http://10.0.2.2:8081/";
+    // 当前应用开通的华为应用内支付公钥
+    public static final String IAP_PUBLIC_KEY = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtffauJMwskVv8vV4bmW0Mi6S+mZpRdqSyfMzLB11JMq8TmZkfeWQHJccwJmDu9Cp3MPEAyAlTWQHIX15AkuseyKZqnbbAbQC4NsmoRbuVi7S4Dx681xpdJP5eg+zKXffnL+YFw2/KU1P7xl25IaISg+5KLT8fjDezJte56+CgCTvHWq495BTDwvz9hGK7kGZXS7V2rJk/0Hjol1IhuPE9WJGgm5YbaJk+78hp6LHE8idZOTUmtJsXPvsVW1PEIdztr47gpS0ieyu8NLCga3Ye0Q5RHEuvrzkeF/AJrsGBJsn7JM7N7buTj5D8+ZW0sSOnvnt9eXdPpIiKO62Vyd6cVFoCC7hpLx6VfJbkiloxzZiNqsyw3A69Kd494kqVFzM1cJu8KgYZaGPVatlf0JXoVZVZcfrLjLrUNjtrXMMQM84nre6DmUtoplgSEryA8XlpLzwjjTf0NjMTrM5YGEfwHI8UC+NVzmEPP4Q8fDaz0vzwN0tVx27pmTsDJSg5pUZAgMBAAE=";
+    // 订单已完成交易(支付并已发货)的状态码
+    public static final Integer ORDER_STATUS_SUCCESS = 1;
+    // 订单尚未支付的状态码
+    public static final Integer ORDER_STATUS_UNPAID = 2;
+    // 订单已支付等待发货的状态码
+    public static final Integer ORDER_STATUS_WAIT = 3;
+    // 订单已取消的状态码
+    public static final Integer ORDER_STATUS_CANCEL = 4;
+    // AppGalleryConnect里配置的应用内商品ID
+    public static final String PRODUCT_ID_FOR_10_DIAMONDS = "test_commodity_202012_01";
+    // 请求操作成功返回码 - 用于ResultVO
+    public static final Integer REQUEST_SUCCESS = 1;
+    // 请求操作失败返回码 - 用于ResultVO
+    public static final Integer REQUEST_FAIL = 0;
+}

+ 19 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/DateUtil.java

@@ -0,0 +1,19 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class DateUtil {
+    private final static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
+
+    public static String getDateString(Date date){
+        return df.format(date);
+    }
+
+    public static Date toDate(String dateString) throws ParseException {
+        return df.parse(dateString);
+    }
+}

+ 20 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/KeyboardUtil.java

@@ -0,0 +1,20 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import android.content.Context;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ * 用于显示或隐藏输入法键盘
+ */
+public class KeyboardUtil {
+    public static void hide(Context context, View view) {
+        InputMethodManager im = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+        im.hideSoftInputFromWindow(view.getWindowToken(),0);
+    }
+
+    public static void show(Context context,View view) {
+        InputMethodManager im = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+        im.showSoftInput(view,0);
+    }
+}

+ 5 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/LoadState.java

@@ -0,0 +1,5 @@
+package cn.seecoder.courselearningdemo.utils;
+
+public enum LoadState {
+    LOADING, SUCCESS, EMPTY, ERROR
+}

+ 51 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/LogUtil.java

@@ -0,0 +1,51 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * 日志工具类
+ */
+public class LogUtil {
+
+    private static int level = 2;
+    private static final int VERBOSE = 1;
+    private static final int DEBUG = 2;
+    private static final int INFO = 3;
+    private static final int WARN = 4;
+    private static final int ERROR = 5;
+
+    public static void v(Object object, String message) {
+        if(level <= VERBOSE) {
+            Log.v(objTag(object), message);
+        }
+    }
+
+    public static void d(Object object, String message) {
+        if(level <= DEBUG) {
+            Log.d(objTag(object), message);
+        }
+    }
+
+    public static void i(Object object, String message) {
+        if(level <= INFO) {
+            Log.i(objTag(object), message);
+        }
+    }
+
+    public static void w(Object object, String message) {
+        if(level <= WARN) {
+            Log.w(objTag(object), message);
+        }
+    }
+
+    public static void e(Object object, String message) {
+        if(level <= ERROR) {
+            Log.e(objTag(object), message);
+        }
+    }
+
+    public static String objTag(Object obj){
+        return obj instanceof String ? obj.toString():obj.getClass().getSimpleName();
+    }
+}

+ 5 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/RechargeState.java

@@ -0,0 +1,5 @@
+package cn.seecoder.courselearningdemo.utils;
+
+public enum RechargeState {
+    INITIALIZING, UNSUPPORTED, ENV_ERROR, PRODUCT_ERROR, INITIALIZED, PURCHASED, SUCCEED, CANCELED, REFUNDED
+}

+ 11 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/SizeUtil.java

@@ -0,0 +1,11 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import android.content.Context;
+
+public class SizeUtil {
+    public static int dip2px(Context context,float dpValue) {
+        float scale = context.getResources().getDisplayMetrics().density;
+        return (int) (dpValue * scale + 0.5f);
+    }
+
+}

+ 7 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/StringUtil.java

@@ -0,0 +1,7 @@
+package cn.seecoder.courselearningdemo.utils;
+
+public class StringUtil {
+    public static boolean hasText(String str){
+        return str != null && str.length() > 0 && str.trim().length() > 0;
+    }
+}

+ 26 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/ToastUtil.java

@@ -0,0 +1,26 @@
+package cn.seecoder.courselearningdemo.utils;
+
+import android.widget.Toast;
+
+import cn.seecoder.courselearningdemo.CourseLearningDemoApplication;
+
+public class ToastUtil {
+    private static Toast toast;
+    public static void showShortToast(String msg) {
+        if (toast == null) {
+            toast = Toast.makeText(CourseLearningDemoApplication.getContext(), msg, Toast.LENGTH_SHORT);
+        } else {
+            toast.setText(msg);
+        }
+        toast.show();
+    }
+
+    public static void showLongToast(String msg) {
+        if (toast == null) {
+            toast = Toast.makeText(CourseLearningDemoApplication.getContext(), msg, Toast.LENGTH_LONG);
+        } else {
+            toast.setText(msg);
+        }
+        toast.show();
+    }
+}

+ 16 - 0
app/src/main/java/cn/seecoder/courselearningdemo/utils/UrlUtil.java

@@ -0,0 +1,16 @@
+package cn.seecoder.courselearningdemo.utils;
+
+public class UrlUtil {
+
+    public static String getCourseImageUrl(String pictureName) {
+        return Constant.HOST + pictureName;
+    }
+
+//    public static String getCoverPath(String pict_url) {
+//        if(pict_url.startsWith("http") || pict_url.startsWith("https")) {
+//            return pict_url;
+//        } else {
+//            return "https:" + pict_url;
+//        }
+//    }
+}

+ 89 - 0
app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/CourseViewModel.java

@@ -0,0 +1,89 @@
+package cn.seecoder.courselearningdemo.viewmodel;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.dao.UserDao;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.model.CourseWare;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class CourseViewModel extends ViewModel {
+    private final MutableLiveData<Course> courseData = new MutableLiveData<>();
+    private final MutableLiveData<List<CourseWare>> courseWareData = new MutableLiveData<>();
+
+    public void setCourse(Course course){
+        this.courseData.setValue(course);
+        loadCourseWareData();
+    }
+
+    public void loadCourse(Integer courseId){
+        String uid = null;
+        User user = UserDao.getUser();
+        if(user!=null && user.getUid()!=null)
+            uid = user.getUid();
+        Call<Course> courseCall = RetrofitClient.courseService.getCourse(courseId, uid);
+        courseCall.enqueue(new Callback<Course>() {
+            @Override
+            public void onResponse(Call<Course> call, Response<Course> response) {
+//                LogUtil.i("loadCourse ", response.body().toString());
+                if(response.body() == null)
+                    ToastUtil.showShortToast("后端数据异常...");
+                else {
+                    courseData.setValue(response.body());
+                    loadCourseWareData();
+                }
+            }
+
+            @Override
+            public void onFailure(Call<Course> call, Throwable t) {
+                LogUtil.e(courseCall, "loadCourse failure: "+t.getMessage() + "  caused by: "+t.getCause());
+                ToastUtil.showShortToast("网络错误,请稍后重试!");
+            }
+        });
+    }
+
+    public LiveData<Course> getCourseData(){
+        return courseData;
+    }
+
+    public LiveData<List<CourseWare>> getCourseWareData(){
+        return courseWareData;
+    }
+
+    private void loadCourseWareData(){
+        Course course = courseData.getValue();
+        String uid = null;
+        User user = UserDao.getUser();
+        if(user!=null && user.getUid()!=null)
+            uid = user.getUid();
+        if(course==null || course.getId()==null)
+            return;
+        Call<List<CourseWare>> courseWareCall = RetrofitClient.courseWareService.getCourseWare(course.getId(), uid);
+        courseWareCall.enqueue(new Callback<List<CourseWare>>() {
+            @Override
+            public void onResponse(Call<List<CourseWare>> call, Response<List<CourseWare>> response) {
+                if(response.body() == null)
+                    ToastUtil.showShortToast("后端数据异常...");
+                else
+                    courseWareData.setValue(response.body());
+            }
+
+            @Override
+            public void onFailure(Call<List<CourseWare>> call, Throwable t) {
+                LogUtil.e(courseWareCall, "loadCourseWareData failure: "+t.getMessage() + "  caused by: "+t.getCause());
+                ToastUtil.showShortToast("网络错误,请稍后重试!");
+            }
+        });
+    }
+
+}

+ 150 - 0
app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/HomeViewModel.java

@@ -0,0 +1,150 @@
+package cn.seecoder.courselearningdemo.viewmodel;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.CourseOrder;
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import cn.seecoder.courselearningdemo.utils.Constant;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.model.PageInfo;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.utils.LoadState;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+
+public class HomeViewModel extends ViewModel {
+    private String uid = null;
+    private String queryKey = "";
+    private Integer currentPage = 1;
+    private MutableLiveData<LoadState> stateData;
+    private MutableLiveData<List<Course>> courseData;
+    private final MutableLiveData<Boolean> courseOrderFlag = new MutableLiveData<>();
+
+    public void setUid(String uid) {
+        this.uid = uid;
+        query();
+    }
+
+    public String getUid() {
+        return uid;
+    }
+
+    public void setQueryKey(String queryKey) {
+        this.queryKey = queryKey.trim();
+    }
+
+    public LiveData<LoadState> getState(){
+        return stateData;
+    }
+
+    public LiveData<List<Course>> getData() {
+        if (courseData == null) {
+            courseData = new MutableLiveData<>();
+            stateData = new MutableLiveData<>();
+            loadData();
+        }
+        return courseData;
+    }
+
+    public LiveData<Boolean> getCourseOrderFlag(){
+        return courseOrderFlag;
+    }
+    public void getNextPageData(){
+        currentPage ++;
+        loadData();
+    }
+
+    public void query(){
+        currentPage = 1;
+        courseData.setValue(new ArrayList<>());
+        loadData();
+    }
+
+    private void loadData(){
+        stateData.setValue(LoadState.LOADING);
+        Call<PageInfo<Course>> courseCall =  RetrofitClient.courseService.getAllCourses(currentPage, uid, queryKey);
+        // 异步调用
+        courseCall.enqueue(new Callback<PageInfo<Course>>() {
+            @Override
+            public void onResponse(Call<PageInfo<Course>> call, Response<PageInfo<Course>> response) {
+                LogUtil.i(courseCall, "Get response");
+                if (response.body() == null){
+                    stateData.setValue(LoadState.ERROR);
+                    currentPage --;
+                } else if (response.body().getSize() == 0 || currentPage > response.body().getPages() ){
+                    stateData.setValue(LoadState.EMPTY);
+                    currentPage --;
+                } else {
+                    stateData.setValue(LoadState.SUCCESS);
+                    List<Course> temp = (courseData.getValue() != null) ? courseData.getValue() : new ArrayList<>();
+                    temp.addAll(response.body().getList());
+                    courseData.setValue(temp);
+                }
+            }
+
+            @Override
+            public void onFailure(Call<PageInfo<Course>> call, Throwable t) {
+                LogUtil.e(courseCall, "loadCourseListPageInfo failure: "+ t.getMessage() + "  caused by: "+t.getCause());
+                stateData.setValue(LoadState.ERROR);
+                currentPage --;
+            }
+        });
+    }
+
+    public void createCourseOrder(Course course){
+        CourseOrder order = new CourseOrder();
+        order.setUserId(uid);
+        order.setCourseId(course.getId());
+        order.setCourseName(course.getName());
+        order.setCost(course.getCost());
+        order.setStatus(Constant.ORDER_STATUS_SUCCESS);
+        Call<ResultVO<CourseOrder>> courseOrderCall = RetrofitClient.courseOrderService.createCourseOrder(order);
+        courseOrderCall.enqueue(new Callback<ResultVO<CourseOrder>>() {
+            @Override
+            public void onResponse(Call<ResultVO<CourseOrder>> call, Response<ResultVO<CourseOrder>> response) {
+                if(response.body() == null || response.body().getCode() == null)
+                    LogUtil.e("createCourseOrder", "response Null Error!");
+                else if(response.body().getCode().equals(Constant.REQUEST_SUCCESS)){
+                    courseOrderFlag.setValue(true);
+                    sendCourseOrderSuccessNotify(course.getId());
+                }else {
+                    courseOrderFlag.setValue(false);
+                    LogUtil.e("createCourseOrder", response.body().getMsg());
+                }
+            }
+
+            @Override
+            public void onFailure(Call<ResultVO<CourseOrder>> call, Throwable t) {
+                LogUtil.e("createCourseOrder Fail", t.getMessage());
+            }
+        });
+    }
+
+    private void sendCourseOrderSuccessNotify(Integer courseId){
+        System.out.println("sendCourseOrderSuccessNotify uid = "+uid);
+        Call<ResultVO<String>> sendCourseOrderSuccessNotify = RetrofitClient.notifyService.sendCourseOrderSuccessNotify(uid, courseId);
+        sendCourseOrderSuccessNotify.enqueue(new Callback<ResultVO<String>>() {
+            @Override
+            public void onResponse(Call<ResultVO<String>> call, Response<ResultVO<String>> response) {
+                if(response.body() == null || response.body().getCode() == null)
+                    LogUtil.e("sendCourseOrderSuccessNotify", "response Null Error!");
+                else if(response.body().getCode().equals(Constant.REQUEST_SUCCESS))
+                    LogUtil.i("sendCourseOrderSuccessNotify", "success");
+                else
+                    LogUtil.i("sendCourseOrderSuccessNotify", "fail");
+            }
+
+            @Override
+            public void onFailure(Call<ResultVO<String>> call, Throwable t) {
+                LogUtil.i("sendCourseOrderSuccessNotify", "Call fail"+ t.getMessage() + "  caused by: "+t.getCause());
+            }
+        });
+    }
+}

+ 292 - 0
app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/RechargeViewModel.java

@@ -0,0 +1,292 @@
+package cn.seecoder.courselearningdemo.viewmodel;
+
+import com.huawei.hmf.tasks.OnFailureListener;
+import com.huawei.hmf.tasks.OnSuccessListener;
+import com.huawei.hmf.tasks.Task;
+import com.huawei.hms.iap.IapApiException;
+import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseReq;
+import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseResult;
+import com.huawei.hms.iap.entity.InAppPurchaseData;
+import com.huawei.hms.iap.entity.IsEnvReadyResult;
+import com.huawei.hms.iap.entity.OrderStatusCode;
+import com.huawei.hms.iap.entity.OwnedPurchasesReq;
+import com.huawei.hms.iap.entity.OwnedPurchasesResult;
+import com.huawei.hms.iap.entity.ProductInfo;
+import com.huawei.hms.iap.entity.ProductInfoReq;
+import com.huawei.hms.iap.entity.PurchaseIntentReq;
+import com.huawei.hms.iap.entity.PurchaseIntentResult;
+import com.huawei.hms.support.api.client.Status;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.huawei.hms.iap.IapClient;
+import com.huawei.hms.iap.entity.ProductInfoResult;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.RechargeOrder;
+import cn.seecoder.courselearningdemo.model.ResultVO;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.utils.CipherUtil;
+import cn.seecoder.courselearningdemo.utils.Constant;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.utils.RechargeState;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class RechargeViewModel extends ViewModel {
+    private IapClient client;
+    private String uid = null;
+    private boolean envReady = false;
+    private ProductInfo productInfo = null;
+    private final MutableLiveData<RechargeState> rechargeStateData = new MutableLiveData<>();
+    // 用于拉起华为IAP收银台页面的status
+    private Status resolutionStatus = null;
+
+    public void init(IapClient client, String uid){
+        this.client = client;
+        this.uid = uid;
+        // 一旦设定了IapClient 就开始判断是否支持应用内支付
+        queryIsEnvReady();
+    }
+
+    public void resetEnvReady(){
+        envReady = false;
+    }
+
+    public boolean isEnvReady(){
+        return envReady;
+    }
+
+    public ProductInfo getProductInfo(){
+        return productInfo;
+    }
+
+    public LiveData<RechargeState> getRechargeStateData() {
+        return rechargeStateData;
+    }
+
+    public Status getResolutionStatus(){
+        return resolutionStatus;
+    }
+
+    // 判断当前环境是否支持应用内支付
+    public void queryIsEnvReady(){
+        rechargeStateData.setValue(RechargeState.INITIALIZING);
+        Task<IsEnvReadyResult> task = client.isEnvReady();
+        task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
+            @Override
+            public void onSuccess(IsEnvReadyResult result) {
+                envReady = true;
+                // 如果环境支持,则加载商品信息
+                queryProduct();
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                envReady = false;
+                if (e instanceof IapApiException) {
+                    IapApiException apiException = (IapApiException) e;
+                    if (apiException.getStatus().getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
+                        // 用户当前登录的华为帐号所在的服务地不在华为IAP支持结算的国家或地区中
+                        rechargeStateData.setValue(RechargeState.UNSUPPORTED);
+                    }else
+                        rechargeStateData.setValue(RechargeState.ENV_ERROR);
+                    LogUtil.e(task, "queryIsEnvReady fail status code: "+apiException.getStatus().getStatusCode());
+                }else {
+                    LogUtil.e(task, "queryIsEnvReady fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                    rechargeStateData.setValue(RechargeState.ENV_ERROR);
+                }
+            }
+        });
+    }
+
+    // 查询商品
+    public void queryProduct() {
+        List<String> productIdList = new ArrayList<String>();
+        productIdList.add(Constant.PRODUCT_ID_FOR_10_DIAMONDS);
+        ProductInfoReq req = new ProductInfoReq();
+        // priceType: 0:消耗型商品
+        req.setPriceType(0);
+        req.setProductIds(productIdList);
+        Task<ProductInfoResult> task = client.obtainProductInfo(req);
+        task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
+            @Override
+            public void onSuccess(ProductInfoResult result) {
+                if(result.getProductInfoList()==null || result.getProductInfoList().size()==0) {
+                    LogUtil.e(task, "getProductInfoList null or empty!");
+                    rechargeStateData.setValue(RechargeState.PRODUCT_ERROR);
+                    productInfo = null;
+                }
+                else{
+                    // 获取接口请求成功时返回的商品详情信息
+                    productInfo = result.getProductInfoList().get(0);
+                }
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                productInfo = null;
+                rechargeStateData.setValue(RechargeState.PRODUCT_ERROR);
+                if (e instanceof IapApiException) {
+                    IapApiException apiException = (IapApiException) e;
+                    LogUtil.e("getProductInfoList", "fail status code: "+apiException.getStatus().getStatusCode() + " "+ apiException.getMessage());
+                } else {
+                    LogUtil.e("getProductInfoList", "fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                }
+            }
+        });
+    }
+
+    public void createPurchaseIntent(){
+        // 构造一个PurchaseIntentReq对象
+        PurchaseIntentReq req = new PurchaseIntentReq();
+        // 通过createPurchaseIntent接口购买的商品必须是您在AppGallery Connect网站配置的商品。
+        req.setProductId(Constant.PRODUCT_ID_FOR_10_DIAMONDS);
+        // 消耗型商品
+        req.setPriceType(0);
+        req.setDeveloperPayload("test");
+        // 调用createPurchaseIntent接口创建托管商品订单
+        Task<PurchaseIntentResult> task = client.createPurchaseIntent(req);
+        task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
+            @Override
+            public void onSuccess(PurchaseIntentResult result) {
+                if(result == null || result.getStatus() == null) {
+                    LogUtil.e(task, "createPurchaseIntent result or status is null!");
+                }else {
+                    LogUtil.i("createPurchaseIntent", "success");
+                    Status status = result.getStatus();
+                    if(status.hasResolution()) {
+                        resolutionStatus = status;
+                        rechargeStateData.setValue(RechargeState.INITIALIZED);
+                    }
+                }
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                if (e instanceof IapApiException) {
+                    IapApiException apiException = (IapApiException) e;
+                    LogUtil.e("createPurchaseIntent", "fail status code: "+apiException.getStatus().getStatusCode());
+                } else {
+                    LogUtil.e("createPurchaseIntent","fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                }
+            }
+        });
+    }
+
+    public void recharge10Diamonds(String orderId, String purchaseToken){
+        LogUtil.i("开始检查订单对应的钻石是否已发货,未发货则进行发货", "orderId: "+orderId);
+        rechargeStateData.setValue(RechargeState.PURCHASED);
+        RechargeOrder rechargeOrder = new RechargeOrder();
+        rechargeOrder.setUid(uid);
+        rechargeOrder.setOrderId(orderId);
+        rechargeOrder.setValue(10);
+        // 调用应用服务器的recharge接口,若传入的订单id在数据库内已存在,则钻石已经发货过了,否则重新发货
+        Call<ResultVO<User>> rechargeCall = RetrofitClient.userService.recharge(rechargeOrder);
+        rechargeCall.enqueue(new Callback<ResultVO<User>>() {
+            @Override
+            public void onResponse(Call<ResultVO<User>> call, Response<ResultVO<User>> response) {
+                if(response.body() == null || response.body().getCode() == null)
+                    LogUtil.e("rechargeCall", "response Null Error!");
+                else if(response.body().getCode().equals(Constant.REQUEST_SUCCESS)) {
+                    LogUtil.i("rechargeCall success", response.body().getMsg());
+                    ToastUtil.showShortToast(response.body().getMsg());
+                    // 发货成功
+                    rechargeStateData.setValue(RechargeState.SUCCEED);
+                    // 调用消耗型商品确认交易接口
+                    consumeOwnedPurchase(purchaseToken);
+                }
+                else {
+                    LogUtil.e("rechargeCall", response.body().getMsg());
+                }
+            }
+
+            @Override
+            public void onFailure(Call<ResultVO<User>> call, Throwable t) {
+                ToastUtil.showShortToast("网络错误,请稍后重试");
+            }
+        });
+    }
+
+    // (消耗型)商品发货成功后执行消耗操作,通知华为IAP服务器更新对应的状态,使得该消耗型商品可再次购买
+    private void consumeOwnedPurchase(String purchaseToken){
+        // 构造ConsumeOwnedPurchaseReq对象
+        ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
+        req.setPurchaseToken(purchaseToken);
+        // 调用consumeOwnedPurchase接口进行消耗
+        Task<ConsumeOwnedPurchaseResult> task = client.consumeOwnedPurchase(req);
+        task.addOnSuccessListener(new OnSuccessListener<ConsumeOwnedPurchaseResult>() {
+            @Override
+            public void onSuccess(ConsumeOwnedPurchaseResult result) {
+                // 获取接口请求结果
+                LogUtil.d("consumeOwnedPurchase", "success");
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                if (e instanceof IapApiException) {
+                    IapApiException apiException = (IapApiException) e;
+                    LogUtil.e("consumeOwnedPurchase", "fail status code: "+apiException.getStatus().getStatusCode());
+                } else {
+                    LogUtil.e("consumeOwnedPurchase",  "checkOwedPurchases fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                }
+            }
+        });
+    }
+
+    // 检查消耗型商品是否漏单,若漏单则进行补单操作
+    public void checkOwedPurchases(){
+        // 构造一个OwnedPurchasesReq对象
+        OwnedPurchasesReq ownedPurchasesReq = new OwnedPurchasesReq();
+        // priceType: 0:消耗型商品
+        ownedPurchasesReq.setPriceType(0);
+        // 调用obtainOwnedPurchases接口获取所有已购但未发货的消耗型商品
+        Task<OwnedPurchasesResult> task = client.obtainOwnedPurchases(ownedPurchasesReq);
+        task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
+            @Override
+            public void onSuccess(OwnedPurchasesResult result) {
+                // 获取接口请求成功的结果
+                if (result != null && result.getInAppPurchaseDataList() != null) {
+                    for (int i = 0; i < result.getInAppPurchaseDataList().size(); i++) {
+                        String inAppPurchaseData = result.getInAppPurchaseDataList().get(i);
+                        String inAppSignature = result.getInAppSignature().get(i);
+                        // 使用应用的IAP公钥验证inAppPurchaseData的签名数据
+                        // 如果验签成功,确认每个商品的购买状态。确认商品已支付后,检查此前是否已发过货,未发货则进行发货操作。发货成功后执行消耗操作
+                        if(CipherUtil.doCheck(inAppPurchaseData, inAppSignature)) {
+                            try {
+                                LogUtil.i("检查是否漏单","第"+(i+1)+"个商品验签成功");
+                                InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
+                                // 只需要检查purchaseState为0(已购买)的交易项
+                                if(inAppPurchaseDataBean.getPurchaseState()==0 && inAppPurchaseDataBean.getProductId().equals(Constant.PRODUCT_ID_FOR_10_DIAMONDS)){
+                                    recharge10Diamonds(inAppPurchaseDataBean.getOrderID(), inAppPurchaseDataBean.getPurchaseToken());
+                                }
+                            } catch (JSONException e) {
+                                LogUtil.e("checkOwedPurchases", "InAppPurchaseData JSON parse error");
+                            }
+                        }
+                    }
+                }
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                if (e instanceof IapApiException) {
+                    IapApiException apiException = (IapApiException) e;
+                    LogUtil.e("checkOwedPurchases", "fail status code: "+apiException.getStatus().getStatusCode());
+                } else {
+                    LogUtil.e("checkOwedPurchases",  "checkOwedPurchases fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                }
+            }
+        });
+    }
+}

+ 179 - 0
app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/SharedViewModel.java

@@ -0,0 +1,179 @@
+package cn.seecoder.courselearningdemo.viewmodel;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.huawei.hmf.tasks.OnCompleteListener;
+import com.huawei.hmf.tasks.OnFailureListener;
+import com.huawei.hmf.tasks.OnSuccessListener;
+import com.huawei.hmf.tasks.Task;
+import com.huawei.hms.common.ApiException;
+import com.huawei.hms.support.account.AccountAuthManager;
+import com.huawei.hms.support.account.request.AccountAuthParams;
+import com.huawei.hms.support.account.request.AccountAuthParamsHelper;
+import com.huawei.hms.support.account.result.AuthAccount;
+import com.huawei.hms.support.account.service.AccountAuthService;
+
+import cn.seecoder.courselearningdemo.CourseLearningDemoApplication;
+import cn.seecoder.courselearningdemo.dao.UserDao;
+import cn.seecoder.courselearningdemo.model.User;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * SharedViewModel 和MainActivity生命周期绑定,可存储如用户信息等多个View共享的信息。
+ */
+public class SharedViewModel extends ViewModel {
+    private MutableLiveData<String> uid;
+    private final MutableLiveData<User> userData = new MutableLiveData<>();
+    private AccountAuthService mAuthManager;
+
+    public LiveData<String> getUid(){
+        if(uid == null){
+            uid = new MutableLiveData<>();
+            // 打开APP时,默认使用silentSignIn尝试登录
+            silentSignIn();
+        }
+        return uid;
+    }
+
+    public LiveData<User> getUser(){
+        return userData;
+    }
+
+    public void updateUserData(User user){
+//        LogUtil.e(this, "updateUserData  user: "+user);
+        if(user==null || user.getUid()==null){
+            uid.setValue(null);
+            UserDao.clearUser();
+        }else{
+            userData.setValue(user);
+            UserDao.saveUser(user);
+            if(uid.getValue() == null || !uid.getValue().equals(user.getUid()))
+                uid.setValue(user.getUid());
+        }
+    }
+
+    // 静默登录
+    private void silentSignIn() {
+        AccountAuthParams mAuthParam = new AccountAuthParamsHelper(AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM).createParams();
+        mAuthManager = AccountAuthManager.getService(CourseLearningDemoApplication.getContext(), mAuthParam);
+        Task<AuthAccount> task = mAuthManager.silentSignIn();
+        task.addOnSuccessListener(new OnSuccessListener<AuthAccount>() {
+            @Override
+            public void onSuccess(AuthAccount authAccount) {
+                LogUtil.i(task, "Silent sign in success");
+                User user = new User();
+                user.setUid(authAccount.getUnionId());
+                user.setUname(authAccount.getDisplayName());
+                user.setPicture(authAccount.getAvatarUriString());
+//                LogUtil.i(task, "User Info "+user);
+                updateUserData(user);
+            }
+        });
+        task.addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                // 如果静默登录失败
+                if (e instanceof ApiException) {
+                    UserDao.clearUser();
+                    ApiException apiException = (ApiException) e;
+                    LogUtil.w(task, "Silent sign in error, status code: " + apiException.getStatusCode());
+                    LogUtil.w(task, "Silent sign in error, " + apiException.getMessage());
+                }
+            }
+        });
+    }
+
+    public AccountAuthService getAccountAuthServiceForSignIn() {
+        AccountAuthParams mAuthParam = new AccountAuthParamsHelper(AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
+                .setIdToken()
+                .setAccessToken()
+                .createParams();
+        mAuthManager = AccountAuthManager.getService(CourseLearningDemoApplication.getContext(), mAuthParam);
+        return mAuthManager;
+    }
+
+    /**
+     * 用户登出
+     * 在一个成功的登出操作之前,我们将所有登录操作所使用的AccountAuthService实例 都同步到mAuthManager
+     */
+    public void signOut() {
+        Task<Void> signOutTask = mAuthManager.signOut();
+        signOutTask.addOnSuccessListener(new OnSuccessListener<Void>() {
+            @Override
+            public void onSuccess(Void aVoid) {
+                updateUserData(new User());
+                LogUtil.i(signOutTask, "Sign out success.");
+            }
+        }).addOnFailureListener(new OnFailureListener() {
+            @Override
+            public void onFailure(Exception e) {
+                LogUtil.d(signOutTask, "Sign out fail!");
+            }
+        });
+    }
+
+    public void cancelAuthorization(){
+        mAuthManager.cancelAuthorization().addOnCompleteListener(new OnCompleteListener<Void>() {
+            @Override
+            public void onComplete(Task<Void> task) {
+                updateUserData(new User());
+                if (task.isSuccessful()) {
+                    //取消授权成功后的处理
+                    LogUtil.i("cancelAuthorization", "success");
+                } else {
+                    //异常处理
+                    Exception e = task.getException();
+                    if (e instanceof ApiException)
+                        LogUtil.e("cancelAuthorization", "fail status code:"+ ((ApiException) e).getStatusCode());
+                    else
+                        LogUtil.e("consumeOwnedPurchase",  "checkOwedPurchases fail failure: "+e.getMessage() + "  caused by: "+e.getCause());
+                }
+            }
+        });
+    }
+
+    public void userLoginToServer() {
+        Call<User> userCall = RetrofitClient.userService.login(userData.getValue());
+        userCall.enqueue(new Callback<User>() {
+            @Override
+            public void onResponse(Call<User> call, Response<User> response) {
+//                LogUtil.i("userLoginToServer", "get response: "+response.body());
+                if(response.body() == null || response.body().getUid() == null)
+                    LogUtil.e("userLoginToServer", "response Null Error!");
+                else
+                    updateUserData(response.body());
+            }
+
+            @Override
+            public void onFailure(Call<User> call, Throwable t) {
+                LogUtil.e("userLoginToServer", "Call failure: "+ t.getMessage() + "  caused by: "+t.getCause());
+            }
+        });
+    }
+
+    // 从应用服务器重新拉取用户账号信息
+    public void reloadUserDataFromServer() {
+        Call<User> userCall = RetrofitClient.userService.getUser(uid.getValue());
+        userCall.enqueue(new Callback<User>() {
+            @Override
+            public void onResponse(Call<User> call, Response<User> response) {
+                LogUtil.i("reloadUserDataFromServer", "get response");
+                if(response.body() == null || response.body().getUid() == null)
+                    LogUtil.e("reloadUserDataFromServer", "response Null Error!");
+                else
+                    updateUserData(response.body());
+            }
+
+            @Override
+            public void onFailure(Call<User> call, Throwable t) {
+                LogUtil.e("reloadUserDataFromServer", "Call failure: "+ t.getMessage() + "  caused by: "+t.getCause());
+            }
+        });
+    }
+}

+ 55 - 0
app/src/main/java/cn/seecoder/courselearningdemo/viewmodel/StudyViewModel.java

@@ -0,0 +1,55 @@
+package cn.seecoder.courselearningdemo.viewmodel;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.List;
+
+import cn.seecoder.courselearningdemo.model.Course;
+import cn.seecoder.courselearningdemo.network.RetrofitClient;
+import cn.seecoder.courselearningdemo.utils.LogUtil;
+import cn.seecoder.courselearningdemo.utils.ToastUtil;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class StudyViewModel extends ViewModel {
+    private String uid = null;
+    private final MutableLiveData<List<Course>> courseData = new MutableLiveData<>();;
+
+    public void setUid(String uid) {
+        this.uid = uid;
+        loadData();
+    }
+
+    public String getUid() {
+        return uid;
+    }
+
+    public LiveData<List<Course>> getData() {
+        return courseData;
+    }
+
+    public void loadData(){
+        if(uid == null) return;
+        LogUtil.i(this, "开始加载用户已购买课程列表");
+        Call<List<Course>> courseCall =  RetrofitClient.courseService.getCourseByUid(uid);
+        // 异步调用
+        courseCall.enqueue(new Callback<List<Course>>() {
+            @Override
+            public void onResponse(Call<List<Course>> call, Response<List<Course>> response) {
+                if (response.body() == null)
+                    ToastUtil.showShortToast("后端数据异常...");
+                courseData.setValue(response.body());
+            }
+
+            @Override
+            public void onFailure(Call<List<Course>> call, Throwable t) {
+                LogUtil.e(courseCall, "getCourseByUid failure: "+ t.getMessage() + "  caused by: "+t.getCause());
+                ToastUtil.showShortToast("网络异常,请稍后重试!");
+            }
+        });
+    }
+
+}

+ 30 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

BIN
app/src/main/res/drawable/account_null.jpg


+ 8 - 0
app/src/main/res/drawable/bg_blue_circle.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners
+        android:radius="5dp"/>
+    <solid
+        android:color="@color/btn_blue"/>
+
+</shape>

+ 9 - 0
app/src/main/res/drawable/bg_blue_circle_side.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="5dp" />
+
+    <stroke
+        android:width="1dp"
+        android:color="@android:color/holo_blue_light" />
+    <solid android:color="@android:color/transparent" />
+</shape>

+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

BIN
app/src/main/res/drawable/ic_push_notification.png


+ 6 - 0
app/src/main/res/drawable/navigation_bar_color.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!--  被选中时显示 colorPrimaryDark, 默认为 colorGrey, 没有状态的需要放到最后 -->
+    <item android:color="@color/colorPrimaryDark" android:state_checked="true" />
+    <item android:color="@color/colorGrey" />
+</selector>

+ 9 - 0
app/src/main/res/drawable/shape_edit_box_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <corners android:radius="4dp" />
+
+    <solid android:color="@color/white" />
+
+</shape>

+ 71 - 0
app/src/main/res/layout/activity_course.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingHorizontal="8dp">
+
+            <TextView
+                android:id="@+id/course_content_name_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="@string/course_name_example"
+                android:textColor="@color/black"
+                android:textSize="22sp" />
+
+            <TextView
+                android:id="@+id/course_content_provider_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="@string/course_provider_example"
+                android:textSize="16sp" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="@string/course_intro_text"
+                android:textColor="@color/black"
+                android:textSize="20sp" />
+
+            <TextView
+                android:id="@+id/course_content_intro_tv"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:ellipsize="end"
+                android:maxLines="2"
+                android:text="@string/course_intro_example"
+                android:textSize="16sp" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="@string/course_ware_text"
+                android:textColor="@color/black"
+                android:textSize="20sp" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/course_ware_content_list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:overScrollMode="never" />
+
+        </LinearLayout>
+    </androidx.core.widget.NestedScrollView>
+
+
+
+
+</LinearLayout>

+ 25 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.activity.MainActivity">
+
+    <FrameLayout
+        android:id="@+id/main_page_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="50dp"/>
+
+    <com.google.android.material.bottomnavigation.BottomNavigationView
+        android:id="@+id/main_navigation_bar"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_alignParentBottom="true"
+        android:background="@color/white"
+        app:menu="@menu/my_navigation_items"
+        app:itemIconTint="@drawable/navigation_bar_color"
+        app:itemTextColor="@drawable/navigation_bar_color"/>
+
+</RelativeLayout>

+ 172 - 0
app/src/main/res/layout/fragment_account.xml

@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <de.hdodenhof.circleimageview.CircleImageView
+        android:id="@+id/user_profile"
+        android:layout_width="96dp"
+        android:layout_height="96dp"
+        android:src="@drawable/account_null"
+        app:civ_border_color="#FF000000"
+        app:civ_border_width="2dp"
+        app:layout_constraintBottom_toTopOf="@+id/guideline"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.35" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.3" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.45" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline3"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.75" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline6"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.9" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.6" />
+
+    <TextView
+        android:id="@+id/login_tv"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:clickable="true"
+        android:text="@string/account_login_hint"
+        android:textSize="22sp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toTopOf="@+id/guideline"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/user_profile" />
+
+    <androidx.constraintlayout.widget.Group
+        android:id="@+id/user_info_group"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="visible"
+        app:constraint_referenced_ids="user_name, user_balance_line"/>
+
+    <TextView
+        android:id="@+id/user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/user_name_text"
+        android:textSize="20sp"
+        app:layout_constraintBottom_toTopOf="@+id/guideline"
+        app:layout_constraintEnd_toStartOf="@+id/guideline5"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/user_profile"/>
+
+
+
+    <LinearLayout
+        android:id="@+id/user_balance_line"
+        android:orientation="horizontal"
+        android:gravity="bottom"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toTopOf="@+id/guideline"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/guideline5"
+        app:layout_constraintTop_toBottomOf="@+id/user_profile">
+
+        <TextView
+            android:id="@+id/user_balance_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/user_balance_text_title"
+            android:textSize="20sp"/>
+
+        <TextView
+            android:id="@+id/user_balance"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/user_balance_text"
+            android:textSize="20sp"
+            app:drawableRightCompat="@mipmap/diamond_medium" />
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/account_courses_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:text="@string/account_courses_text"
+        android:visibility="gone"
+        app:layout_constraintBottom_toTopOf="@+id/guideline2"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/guideline" />
+
+    <Button
+        android:id="@+id/account_orders_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:text="@string/account_orders_text"
+        android:visibility="gone"
+        app:layout_constraintBottom_toTopOf="@+id/guideline4"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/guideline2" />
+
+    <Button
+        android:id="@+id/account_recharge_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:text="@string/recharge_text"
+        android:textColor="@color/purple_500"
+        app:layout_constraintBottom_toTopOf="@+id/guideline3"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/guideline4" />
+
+    <Button
+        android:id="@+id/account_logout_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:text="@string/logout_text"
+        android:textColor="@color/warning_red"
+        app:layout_constraintBottom_toTopOf="@+id/guideline6"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/guideline3" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline5"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.5"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 57 - 0
app/src/main/res/layout/fragment_home.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="40dp"
+        android:background="@color/colorPrimaryDark"
+        android:gravity="center_vertical">
+
+        <TextView
+            android:id="@+id/logo"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="10dp"
+            android:layout_marginTop="4dp"
+            android:text="@string/logo_text"
+            android:textColor="@color/white"
+            android:textSize="16sp" />
+
+        <EditText
+            android:id="@+id/search_course"
+            android:layout_width="match_parent"
+            android:layout_height="30dp"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="10dp"
+            android:layout_toEndOf="@id/logo"
+            android:background="@drawable/shape_edit_box_bg"
+            android:focusable="true"
+            android:imeOptions="actionSearch"
+            android:hint="@string/text_search_tips"
+            android:maxLines="1"
+            android:singleLine="true"
+            android:paddingLeft="10dp"
+            android:paddingRight="10dp"
+            android:textSize="14sp"
+            android:autofillHints="软件工程" />
+
+    </RelativeLayout>
+
+    <com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout
+        android:id="@+id/course_list_refresh_layout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/course_list_rv"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:overScrollMode="never" />
+
+    </com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>
+
+</LinearLayout>

+ 67 - 0
app/src/main/res/layout/fragment_study.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingHorizontal="8dp">
+            <RelativeLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="10dp"
+                    android:text="@string/account_courses_text"
+                    android:textColor="@color/black"
+                    android:textSize="22sp" />
+
+                <Button
+                    android:id="@+id/account_courses_fresh_btn"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentEnd="true"
+                    android:text="@string/account_courses_refresh_button"/>
+            </RelativeLayout>
+
+
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/my_course_list_rv"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:overScrollMode="never"
+                android:visibility="gone"/>
+
+        </LinearLayout>
+    </androidx.core.widget.NestedScrollView>
+
+    <TextView
+        android:id="@+id/none_bought_tv"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="@string/none_bought_text"
+        android:textSize="18sp"
+        android:visibility="gone"
+        android:layout_centerVertical="true"
+        android:layout_centerHorizontal="true"/>
+
+    <TextView
+        android:id="@+id/login_warn_tv"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="@string/login_warn_text"
+        android:textSize="18sp"
+        android:layout_centerVertical="true"
+        android:layout_centerHorizontal="true"/>
+
+</RelativeLayout>

+ 73 - 0
app/src/main/res/layout/item_course_content.xml

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    app:cardBackgroundColor="@color/white"
+    app:cardElevation="4dp"
+    app:contentPadding="8dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:id="@+id/course_item_picture"
+            android:layout_height="wrap_content"
+            android:src="@mipmap/ic_launcher" />
+
+        <TextView
+            android:id="@+id/course_item_name_tv"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="@string/course_name_example"
+            android:textColor="@color/dark_red"
+            android:textSize="18sp" />
+
+        <TextView
+            android:id="@+id/course_item_intro_tv"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="6dp"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:text="@string/course_intro_example"
+            android:textSize="14sp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="6dp"
+            android:orientation="horizontal">
+
+            <Button
+                android:id="@+id/course_item_buy_btn"
+                android:layout_width="wrap_content"
+                android:layout_height="40dp"
+                android:background="@color/btn_orange"
+                android:textColor="@color/white"
+                android:paddingHorizontal="20dp"
+                android:visibility="visible"
+                android:layout_marginRight="20dp"
+                android:drawableRight="@mipmap/diamond_small"
+                android:text="@string/course_buy_button_example" />
+
+            <Button
+                android:id="@+id/course_item_show_btn"
+                android:layout_width="wrap_content"
+                android:layout_height="40dp"
+                android:background="@color/btn_orange"
+                android:textColor="@color/white"
+                android:paddingHorizontal="40dp"
+                android:text="@string/course_show_button_example" />
+
+        </LinearLayout>
+
+
+    </LinearLayout>
+
+</androidx.cardview.widget.CardView>

+ 35 - 0
app/src/main/res/layout/item_course_ware_content.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    app:cardBackgroundColor="@color/white"
+    app:cardElevation="4dp"
+    app:contentPadding="8dp">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/course_ware_item_name_tv"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentStart="true"
+            android:layout_centerVertical="true"
+            android:text="@string/course_ware_name_example"
+            android:layout_marginEnd="26dp"
+            android:textSize="18sp" />
+
+        <ImageView
+            android:id="@+id/course_ware_locked"
+            android:layout_width="26dp"
+            android:layout_height="26dp"
+            android:layout_centerVertical="true"
+            android:layout_alignParentEnd="true"
+            android:src="@mipmap/lock"/>
+    </RelativeLayout>
+
+</androidx.cardview.widget.CardView>

+ 15 - 0
app/src/main/res/menu/my_navigation_items.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/home"
+        android:icon="@mipmap/home_normal"
+        android:title="@string/text_home" />
+    <item
+        android:id="@+id/study"
+        android:icon="@mipmap/course_normal"
+        android:title="@string/text_study" />
+    <item
+        android:id="@+id/account"
+        android:icon="@mipmap/user_normal"
+        android:title="@string/text_account" />
+</menu>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/course_checked.png


BIN
app/src/main/res/mipmap-hdpi/course_normal.png


BIN
app/src/main/res/mipmap-hdpi/course_press.png


BIN
app/src/main/res/mipmap-hdpi/diamond_medium.png


BIN
app/src/main/res/mipmap-hdpi/diamond_small.png


BIN
app/src/main/res/mipmap-hdpi/home_checked.png


BIN
app/src/main/res/mipmap-hdpi/home_normal.png


BIN
app/src/main/res/mipmap-hdpi/home_press.png


BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-hdpi/lock.png


BIN
app/src/main/res/mipmap-hdpi/logo_180x180.png


BIN
app/src/main/res/mipmap-hdpi/search_checked.png


BIN
app/src/main/res/mipmap-hdpi/search_normal.png


BIN
app/src/main/res/mipmap-hdpi/search_press.png


BIN
app/src/main/res/mipmap-hdpi/user_checked.png


BIN
app/src/main/res/mipmap-hdpi/user_normal.png


BIN
app/src/main/res/mipmap-hdpi/user_press.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 16 - 0
app/src/main/res/values-night/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.CourseLearningDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_200</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/black</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_200</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 27 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+
+    <color name="colorPrimaryDark">#303F9F</color>
+
+    <color name="colorAccent">#FF4081</color>
+    <color name="colorGrey">#bfbfbf</color>
+
+    <color name="text_white">#fff</color>
+
+    <color name="dark_red">#D0021B</color>
+
+    <color name="btn_orange">#fc553e</color>
+
+    <color name="warning_red">#F00505</color>
+
+    <color name="btn_blue">#456E94</color>
+
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 6 - 0
app/src/main/res/values/dimens.xml

@@ -0,0 +1,6 @@
+<resources>
+
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_title_margin">10dp</dimen>
+
+</resources>

Некоторые файлы не были показаны из-за большого количества измененных файлов