1.1.8 增加更新弹窗和提示

This commit is contained in:
2026-03-26 10:05:33 +08:00
parent f0e831b2fe
commit 6f0f7c4c09
28 changed files with 963 additions and 147 deletions

View File

@@ -18,8 +18,8 @@ android {
//There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature.
minSdkVersion 23
targetSdkVersion 29
versionCode 16
versionName "1.1.5"
versionCode 19
versionName "1.1.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -9,9 +9,13 @@ import com.hainaos.vc.R;
import com.hainaos.vc.adapter.CategoryAdapter;
import com.hainaos.vc.base.mvvm.BaseMvvmActivity;
import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryUpdateInfo;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.databinding.ActivityCategoryListBinding;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CategoryListActivity extends BaseMvvmActivity<CategoryListViewModel, ActivityCategoryListBinding> {
private static final String TAG = "CategoryListActivity";
@@ -46,8 +50,26 @@ public class CategoryListActivity extends BaseMvvmActivity<CategoryListViewModel
mCategoryAdapter.setCategoryInfos(categoryInfos);
}
});
mViewModel.getCategoryList();
mViewModel.mVideoUpdateMutableLiveData.observe(this, new Observer<VideoUpdate>() {
@Override
public void onChanged(VideoUpdate videoUpdate) {
if (videoUpdate.getHas_new() == 1) {
mViewModel.getCategoryUpdate();
}
}
});
mViewModel.mCategoryUpdateInfoListData.observe(this, new Observer<List<CategoryUpdateInfo>>() {
@Override
public void onChanged(List<CategoryUpdateInfo> categoryUpdateInfos) {
Map<String, Integer> map = categoryUpdateInfos.stream().collect(Collectors.toMap(CategoryUpdateInfo::getCategory_uuid, CategoryUpdateInfo::getCount));
mCategoryAdapter.setCategoryUpdateInfoMap(map);
}
});
mViewModel.getCategoryList();
mViewModel.getVideoUpdate();
}

View File

@@ -7,9 +7,14 @@ import androidx.lifecycle.MutableLiveData;
import com.hainaos.vc.base.mvvm.BaseViewModel;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryUpdateInfo;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.databinding.ActivityCategoryListBinding;
import com.hainaos.vc.network.NetInterfaceManager;
import com.hainaos.vc.utils.TimeUtils;
import com.hjq.toast.Toaster;
import com.tencent.mmkv.MMKV;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
@@ -20,9 +25,10 @@ import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
public class CategoryListViewModel extends BaseViewModel<ActivityCategoryListBinding, ActivityEvent> {
private static final String TAG = "CategoryViewModel";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
@Override
public ActivityCategoryListBinding getVDBinding() {
return binding;
@@ -66,4 +72,75 @@ public class CategoryListViewModel extends BaseViewModel<ActivityCategoryListBin
}
});
}
public MutableLiveData<VideoUpdate> mVideoUpdateMutableLiveData = new MutableLiveData<>();
public void getVideoUpdate() {
String currentTime = TimeUtils.transferMillisecondToDate(System.currentTimeMillis());
String time = mMMKV.decodeString(CommonConfig.CATEGORY_VIDEOS_CHECK_UPDATE_TIME, currentTime);
Log.e(TAG, "getVideoUpdate: " + time);
NetInterfaceManager.getInstance().getVideoUpdateObservable(time)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoUpdate>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getVideoUpdate", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<VideoUpdate> baseResponse) {
Log.e("getVideoUpdate", "onNext: " + baseResponse);
mMMKV.encode(CommonConfig.CATEGORY_VIDEOS_CHECK_UPDATE_TIME, currentTime);
if (baseResponse.code == 200) {
VideoUpdate videoUpdate = baseResponse.data;
mVideoUpdateMutableLiveData.setValue(videoUpdate);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("getVideoUpdate", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("getVideoUpdate", "onComplete: ");
}
});
}
public MutableLiveData<List<CategoryUpdateInfo>> mCategoryUpdateInfoListData = new MutableLiveData<>();
public void getCategoryUpdate() {
String currentTime = TimeUtils.transferMillisecondToDate(System.currentTimeMillis());
String time = mMMKV.decodeString(CommonConfig.CHECK_CATEGORY_UPDATE_NUMBER_TIME, currentTime);
Log.e(TAG, "getCategoryUpdate: " + time);
NetInterfaceManager.getInstance().getCategoryUpdateObservable(time)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<List<CategoryUpdateInfo>>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getCategoryUpdate", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<List<CategoryUpdateInfo>> listBaseResponse) {
Log.e("getCategoryUpdate", "onNext: " + listBaseResponse);
if (listBaseResponse.code == 200) {
List<CategoryUpdateInfo> categoryUpdateInfos = listBaseResponse.data;
mCategoryUpdateInfoListData.setValue(categoryUpdateInfos);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("getCategoryUpdate", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("getCategoryUpdate", "onComplete: ");
}
});
}
}

View File

@@ -17,6 +17,7 @@ import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryVideoInfo;
import com.hainaos.vc.bean.LocalVideoInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.databinding.ActivityCategoryLocalBinding;
import com.hainaos.vc.fragment.passwd.PasswdDialogFragment;
import com.hainaos.vc.utils.FileUtils;
@@ -135,6 +136,18 @@ public class LocalCategoryActivity extends BaseMvvmActivity<LocalCategoryViewMod
}
}
});
mViewModel.mVideoUpdateMutableLiveData.observe(this, new Observer<VideoUpdate>() {
@Override
public void onChanged(VideoUpdate videoUpdate) {
if (videoUpdate.getHas_new() == 1) {
mViewDataBinding.ivUpdate.setVisibility(View.VISIBLE);
} else {
mViewDataBinding.ivUpdate.setVisibility(View.GONE);
}
}
});
if (TextUtils.isEmpty(mPasswd)) {
mViewModel.getVideoList(mCategoryInfo.getFolder());
} else {

View File

@@ -8,10 +8,14 @@ import com.hainaos.vc.base.mvvm.BaseViewModel;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.LocalVideoInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.databinding.ActivityCategoryLocalBinding;
import com.hainaos.vc.network.NetInterfaceManager;
import com.hainaos.vc.utils.FileUtils;
import com.hainaos.vc.utils.TimeUtils;
import com.hainaos.vc.utils.VideoUtils;
import com.tencent.mmkv.MMKV;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
@@ -33,9 +37,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import wseemann.media.FFmpegMediaMetadataRetriever;
public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalBinding, ActivityEvent> {
private static final String TAG = "LocalCategoryViewModel";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
@Override
public ActivityCategoryLocalBinding getVDBinding() {
return binding;
@@ -49,6 +54,7 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
public MutableLiveData<BaseResponse<VideoListData>> mCategoryVideoInfoListData = new MutableLiveData<>();
public void getVideoList(String uuid, String password) {
// getVideoUpdate(uuid);
NetInterfaceManager.getInstance().getVideoListObservable(uuid, password)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoListData>>() {
@@ -138,6 +144,7 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<ArrayList<LocalVideoInfo>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
@@ -164,4 +171,40 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
});
}
public MutableLiveData<VideoUpdate> mVideoUpdateMutableLiveData = new MutableLiveData<>();
public void getVideoUpdate(String uuid) {
String currentTime = TimeUtils.transferMillisecondToDate(System.currentTimeMillis());
String time = mMMKV.decodeString(uuid, currentTime);
Log.e(TAG, "getVideoUpdate: " + time);
NetInterfaceManager.getInstance().getVideoUpdateObservable(time)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoUpdate>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getVideoUpdate", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<VideoUpdate> baseResponse) {
Log.e("getVideoUpdate", "onNext: " + baseResponse);
mMMKV.encode(uuid, currentTime);
if (baseResponse.code == 200) {
VideoUpdate videoUpdate = baseResponse.data;
mVideoUpdateMutableLiveData.setValue(videoUpdate);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("getVideoUpdate", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("getVideoUpdate", "onComplete: ");
}
});
}
}

View File

@@ -17,6 +17,7 @@ import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryVideoInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.databinding.ActivityCategoryVideoBinding;
import com.hainaos.vc.fragment.passwd.PasswdDialogFragment;
@@ -146,6 +147,17 @@ public class CategoryVideoActivity extends BaseMvvmActivity<CategoryVideoViewMod
}
});
mViewModel.mVideoUpdateMutableLiveData.observe(this, new Observer<VideoUpdate>() {
@Override
public void onChanged(VideoUpdate videoUpdate) {
if (videoUpdate.getHas_new() == 1) {
mViewDataBinding.ivUpdate.setVisibility(View.VISIBLE);
} else {
mViewDataBinding.ivUpdate.setVisibility(View.GONE);
}
}
});
mViewModel.getVideoList(mCategoryInfo.getUuid(), mPasswd);
}

View File

@@ -8,9 +8,13 @@ import com.hainaos.vc.base.mvvm.BaseViewModel;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.LocalVideoInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.databinding.ActivityCategoryVideoBinding;
import com.hainaos.vc.network.NetInterfaceManager;
import com.hainaos.vc.utils.FileUtils;
import com.hainaos.vc.utils.TimeUtils;
import com.tencent.mmkv.MMKV;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
@@ -29,6 +33,8 @@ public class CategoryVideoViewModel extends BaseViewModel<ActivityCategoryVideoB
private static final String TAG = "CategoryVideoViewModel";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
@Override
public ActivityCategoryVideoBinding getVDBinding() {
return binding;
@@ -71,6 +77,7 @@ public class CategoryVideoViewModel extends BaseViewModel<ActivityCategoryVideoB
public MutableLiveData<BaseResponse<VideoListData>> mCategoryVideoInfoListData = new MutableLiveData<>();
public void getVideoList(String uuid, String password) {
// getVideoUpdate(uuid);
NetInterfaceManager.getInstance().getVideoListObservable(uuid, password)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoListData>>() {
@@ -97,4 +104,40 @@ public class CategoryVideoViewModel extends BaseViewModel<ActivityCategoryVideoB
}
});
}
public MutableLiveData<VideoUpdate> mVideoUpdateMutableLiveData = new MutableLiveData<>();
public void getVideoUpdate(String uuid) {
String currentTime = TimeUtils.transferMillisecondToDate(System.currentTimeMillis());
String time = mMMKV.decodeString(uuid, currentTime);
Log.e(TAG, "getVideoUpdate: " + time);
NetInterfaceManager.getInstance().getVideoUpdateObservable(time)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoUpdate>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getVideoUpdate", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<VideoUpdate> baseResponse) {
Log.e("getVideoUpdate", "onNext: " + baseResponse);
mMMKV.encode(uuid, currentTime);
if (baseResponse.code == 200) {
VideoUpdate videoUpdate = baseResponse.data;
mVideoUpdateMutableLiveData.setValue(videoUpdate);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("getVideoUpdate", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("getVideoUpdate", "onComplete: ");
}
});
}
}

View File

@@ -10,8 +10,11 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
@@ -21,15 +24,18 @@ import androidx.lifecycle.Observer;
import com.hainaos.vc.BuildConfig;
import com.hainaos.vc.R;
import com.hainaos.vc.activity.category.list.CategoryListActivity;
import com.hainaos.vc.activity.login.LoginActivity;
import com.hainaos.vc.base.BaseFragmentPagerAdapter;
import com.hainaos.vc.base.mvvm.BaseMvvmActivity;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.bean.uiuios.AppUpdateInfo;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.config.Permissions;
import com.hainaos.vc.databinding.ActivityMainBinding;
import com.hainaos.vc.dialog.PermissionsDialog;
import com.hainaos.vc.dialog.PrivacyPolicyDialog;
import com.hainaos.vc.dialog.UpdateDialog;
import com.hainaos.vc.fragment.app.AppFragment;
import com.hainaos.vc.fragment.category.CategoryFragment;
import com.hainaos.vc.utils.ApkUtils;
@@ -40,6 +46,8 @@ import com.hainaos.vc.utils.VideoUtils;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.XXPermissions;
import com.tencent.mmkv.MMKV;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import java.io.File;
import java.io.FileInputStream;
@@ -47,8 +55,12 @@ import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
private static final String TAG = "MainActivity";
@@ -61,6 +73,10 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
private static final int REQUEST_PERMISSION_CODE = 200;
private PublishSubject<Object> clickSubject = PublishSubject.create();
private CompositeDisposable disposables = new CompositeDisposable();
@Override
public boolean setNightMode() {
return true;
@@ -96,6 +112,16 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
@Override
public void initData() {
// 设置防抖500毫秒内只响应最后一次点击
disposables.add(clickSubject
.throttleFirst(10, TimeUnit.MINUTES) // 防抖时间窗口
.observeOn(AndroidSchedulers.mainThread())
.compose(RxLifecycle.bindUntilEvent(lifecycle(), ActivityEvent.DESTROY))
.subscribe(o -> {
// 这里执行需要防抖的操作,例如网络请求、计算等
performAction();
}));
mViewModel.mAppUpdateInfoUiUiOSData.observe(this, new Observer<AppUpdateInfo>() {
@Override
public void onChanged(AppUpdateInfo appUpdateInfo) {
@@ -115,8 +141,26 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
}
}
});
mViewModel.mVideoUpdateMutableLiveData.observe(this, new Observer<VideoUpdate>() {
@Override
public void onChanged(VideoUpdate videoUpdate) {
if (videoUpdate.getHas_new() == 1) {
showUpdateDialog();
} else {
}
}
});
clickSubject.onNext(new Object());
}
private void performAction() {
Log.e(TAG, "performAction: ");
mViewModel.checkUpdateUiUiOS(BuildConfig.APPLICATION_ID);
mViewModel.checkUpdate();
mViewModel.getVideoUpdate();
}
private void initDatas() {
@@ -126,11 +170,51 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
}
private UpdateDialog mUpdateDialog;
private void showUpdateDialog() {
mUpdateDialog = new UpdateDialog(this);
mUpdateDialog.setMessage("更新了新的视频,请去下载")
.setTitle("公告")
.setPositive("点击查看")
.setSingle(true)
.setOnClickBottomListener(new UpdateDialog.OnClickBottomListener() {
@Override
public void onPositiveClick() {
startActivity(new Intent(MainActivity.this, CategoryListActivity.class));
mUpdateDialog.dismiss();
}
@Override
public void onNegtiveClick() {
mUpdateDialog.dismiss();
}
});
Window window = mUpdateDialog.getWindow();
if (window != null) {
window.setGravity(Gravity.CENTER);
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
mUpdateDialog.setCancelable(false);
if (!mUpdateDialog.isShowing()) {
mUpdateDialog.show();
}
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: ");
checkEULA();
clickSubject.onNext(new Object());
}
@Override
protected void onDestroy() {
super.onDestroy();
disposables.clear(); // 避免内存泄漏
}
@Override

View File

@@ -13,10 +13,14 @@ import com.hainaos.vc.base.mvvm.BaseViewModel;
import com.hainaos.vc.bean.AppInfo;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.HomeAppInfo;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.bean.uiuios.AppUpdateInfo;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.databinding.ActivityMainBinding;
import com.hainaos.vc.network.NetInterfaceManager;
import com.hainaos.vc.utils.TimeUtils;
import com.hjq.toast.Toaster;
import com.tencent.mmkv.MMKV;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
@@ -40,6 +44,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEvent> {
private static final String TAG = "MainViewModel";
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
@Override
public ActivityMainBinding getVDBinding() {
return binding;
@@ -84,12 +90,12 @@ public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEv
.subscribe(new Observer<BaseResponse<AppUpdateInfo>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("checkUpdate", "onSubscribe: ");
Log.e("checkUpdateUiUiOS", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<AppUpdateInfo> appUpdateInfoBaseResponse) {
Log.e("checkUpdate", "onNext: " + appUpdateInfoBaseResponse);
Log.e("checkUpdateUiUiOS", "onNext: " + appUpdateInfoBaseResponse);
if (appUpdateInfoBaseResponse.code == 200) {
AppUpdateInfo appUpdateInfo = appUpdateInfoBaseResponse.data;
mAppUpdateInfoUiUiOSData.setValue(appUpdateInfo);
@@ -100,13 +106,13 @@ public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEv
@Override
public void onError(@NonNull Throwable e) {
Log.e("checkUpdate", "onError: ");
Log.e("checkUpdateUiUiOS", "onError: ");
Toaster.show("网络连接失败");
}
@Override
public void onComplete() {
Log.e("checkUpdate", "onComplete: ");
Log.e("checkUpdateUiUiOS", "onComplete: ");
}
});
}
@@ -206,4 +212,40 @@ public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEv
});
}
public MutableLiveData<VideoUpdate> mVideoUpdateMutableLiveData = new MutableLiveData<>();
public void getVideoUpdate() {
String currentTime = TimeUtils.transferMillisecondToDate(System.currentTimeMillis());
String time = mMMKV.decodeString(CommonConfig.MAIN_VIDEOS_CHECK_UPDATE_TIME, currentTime);
Log.e(TAG, "getVideoUpdate: " + time);
NetInterfaceManager.getInstance().getVideoUpdateObservable(time)
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribe(new Observer<BaseResponse<VideoUpdate>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getVideoUpdate", "onSubscribe: ");
}
@Override
public void onNext(@NonNull BaseResponse<VideoUpdate> baseResponse) {
Log.e("getVideoUpdate", "onNext: " + baseResponse);
mMMKV.encode(CommonConfig.MAIN_VIDEOS_CHECK_UPDATE_TIME, currentTime);
if (baseResponse.code == 200) {
VideoUpdate videoUpdate = baseResponse.data;
mVideoUpdateMutableLiveData.setValue(videoUpdate);
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("getVideoUpdate", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("getVideoUpdate", "onComplete: ");
}
});
}
}

View File

@@ -5,6 +5,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
@@ -12,12 +13,13 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import com.hainaos.vc.R;
import com.hainaos.vc.base.mvvm.BaseMvvmActivity;
import com.hainaos.vc.databinding.ActivityDecryptionPlayerBinding;
import com.hainaos.vc.utils.JgyUtils;
import com.hainaos.vc.video.AesDataSource;
import com.hainaos.vc.video.AesDataSource2;
import com.hjq.toast.Toaster;
public class DecryptionPlayerActivity extends BaseMvvmActivity<DecryptionPlayerViewModel, ActivityDecryptionPlayerBinding> {
@@ -46,6 +48,8 @@ public class DecryptionPlayerActivity extends BaseMvvmActivity<DecryptionPlayerV
@Override
protected void initView() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mExoPlayer = new SimpleExoPlayer.Builder(this).build();
mViewDataBinding.playerView.setPlayer(mExoPlayer);
@@ -72,9 +76,9 @@ public class DecryptionPlayerActivity extends BaseMvvmActivity<DecryptionPlayerV
// 创建自定义 Factory
DataSource.Factory dataSourceFactory = () -> {
try {
return new AesDataSource(key, iv);
// DataSource upstream = new FileDataSource();
// return new AesDataSource2(upstream, key, iv);
// return new AesDataSource(key, iv);
DataSource upstream = new FileDataSource();
return new AesDataSource2(upstream, key, iv);
} catch (Exception e) {
Log.e(TAG, "initData: " + e.getMessage());
throw new RuntimeException(e);
@@ -131,6 +135,13 @@ public class DecryptionPlayerActivity extends BaseMvvmActivity<DecryptionPlayerV
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 可选:清除标志以恢复正常息屏行为
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void initializePlayer() {
// if (mExoPlayer == null) {
// // 1. 创建 ExoPlayer 实例

View File

@@ -64,6 +64,8 @@ public class TikTokActivity extends BaseMvvmActivity<TikTokViewModel, ActivityTi
@Override
protected void initView() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Configuration config = getResources().getConfiguration();
oldOrientation = config.orientation;
Log.e(TAG, "orientation first:" + oldOrientation);
@@ -232,5 +234,6 @@ public class TikTokActivity extends BaseMvvmActivity<TikTokViewModel, ActivityTi
mTikTokRecyclerViewAdapter.unRegister();
mTikTokRecyclerViewAdapter = null;
}
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}

View File

@@ -21,6 +21,7 @@ import com.hainaos.vc.fragment.passwd.PasswdDialogFragment;
import com.hainaos.vc.utils.GlideLoadUtils;
import java.util.List;
import java.util.Map;
import me.jessyan.autosize.AutoSizeCompat;
@@ -35,6 +36,13 @@ public class CategoryAdapter extends RecyclerView.Adapter<CategoryAdapter.Holder
notifyDataSetChanged();
}
private Map<String, Integer> mCategoryUpdateInfoMap;
public void setCategoryUpdateInfoMap(Map<String, Integer> categoryUpdateInfoMap) {
mCategoryUpdateInfoMap = categoryUpdateInfoMap;
notifyDataSetChanged();
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@@ -61,6 +69,11 @@ public class CategoryAdapter extends RecyclerView.Adapter<CategoryAdapter.Holder
}
String uuid = categoryInfo.getUuid();
if (mCategoryUpdateInfoMap != null && mCategoryUpdateInfoMap.get(uuid) != null && mCategoryUpdateInfoMap.get(uuid) != 0) {
holder.iv_update.setVisibility(View.VISIBLE);
} else {
holder.iv_update.setVisibility(View.GONE);
}
String icon = categoryInfo.getIcon();
GlideLoadUtils.getInstance().glideLoad(mContext, icon, holder.iv_icon, R.drawable.icon_category);
@@ -91,7 +104,7 @@ public class CategoryAdapter extends RecyclerView.Adapter<CategoryAdapter.Holder
ConstraintLayout root;
TextView tv_app_name;
ImageView iv_icon, iv_lock_icon;
ImageView iv_icon, iv_lock_icon, iv_update;
public Holder(@NonNull View itemView) {
super(itemView);
@@ -100,6 +113,7 @@ public class CategoryAdapter extends RecyclerView.Adapter<CategoryAdapter.Holder
tv_app_name = itemView.findViewById(R.id.tv_app_name);
iv_icon = itemView.findViewById(R.id.iv_icon);
iv_lock_icon = itemView.findViewById(R.id.iv_lock_icon);
iv_update = itemView.findViewById(R.id.iv_update);
}
}

View File

@@ -0,0 +1,26 @@
package com.hainaos.vc.bean;
import java.io.Serializable;
public class CategoryUpdateInfo implements Serializable {
private static final long serialVersionUID = 8109570355122467961L;
String category_uuid;
int count;
public String getCategory_uuid() {
return category_uuid;
}
public void setCategory_uuid(String category_uuid) {
this.category_uuid = category_uuid;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}

View File

@@ -0,0 +1,26 @@
package com.hainaos.vc.bean;
import java.io.Serializable;
public class VideoUpdate implements Serializable {
private static final long serialVersionUID = -1708177371148075587L;
int has_new;
int video_num;
public int getHas_new() {
return has_new;
}
public void setHas_new(int has_new) {
this.has_new = has_new;
}
public int getVideo_num() {
return video_num;
}
public void setVideo_num(int video_num) {
this.video_num = video_num;
}
}

View File

@@ -6,4 +6,13 @@ public class CommonConfig {
/*是否同意隐私协议*/
public static final String AGREED_THE_PRIVACY_AGREEMENT = "agreed_the_privacy_agreement";
/*主页检测是否有视频更新*/
public static final String MAIN_VIDEOS_CHECK_UPDATE_TIME = "MAIN_VIDEOS_CHECK_UPDATE_TIME";
/*分类页面检测是否有视频更新*/
public static final String CATEGORY_VIDEOS_CHECK_UPDATE_TIME = "CATEGORY_VIDEOS_CHECK_UPDATE_TIME";
/*检查分类更新详情时间*/
public static final String CHECK_CATEGORY_UPDATE_NUMBER_TIME = "CHECK_CATEGORY_UPDATE_NUMBER_TIME";
}

View File

@@ -0,0 +1,194 @@
package com.hainaos.vc.dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.hainaos.vc.R;
/**
* description:自定义dialog
*/
public class UpdateDialog extends AlertDialog {
/**
* 显示的标题
*/
private TextView titleTv;
/**
* 显示的消息
*/
private TextView messageTv;
/**
* 确认和取消按钮
*/
private TextView positiveBn;
/**
* 按钮之间的分割线
*/
// private View columnLineView;
private Context mContext;
public UpdateDialog(Context context) {
super(context, R.style.CustomDialog);
this.mContext = context;
}
/**
* 都是内容数据
*/
private String message;
private String title;
private String positive;
private int imageResId = -1;
/**
* 底部是否只有一个按钮
*/
private boolean isSingle = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_update);
//按空白处不能取消动画
setCanceledOnTouchOutside(false);
//初始化界面控件
initView();
//初始化界面数据
refreshView();
//初始化界面控件的事件
initEvent();
}
/**
* 初始化界面的确定和取消监听器
*/
private void initEvent() {
//设置确定按钮被点击后,向外界提供监听
positiveBn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickBottomListener != null) {
onClickBottomListener.onPositiveClick();
}
}
});
}
/**
* 初始化界面控件的显示数据
*/
private void refreshView() {
//如果用户自定了title和message
if (!TextUtils.isEmpty(title)) {
titleTv.setText(title);
titleTv.setVisibility(View.VISIBLE);
} else {
titleTv.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(message)) {
messageTv.setText(message);
}
//如果设置按钮的文字
if (!TextUtils.isEmpty(positive)) {
positiveBn.setText(positive);
} else {
positiveBn.setText("确定");
}
}
@Override
public void show() {
super.show();
refreshView();
}
/**
* 初始化界面控件
*/
private void initView() {
positiveBn = findViewById(R.id.positive);
titleTv = findViewById(R.id.title);
messageTv = findViewById(R.id.message);
}
/**
* 设置确定取消按钮的回调
*/
private OnClickBottomListener onClickBottomListener;
public void setOnClickBottomListener(OnClickBottomListener onClickBottomListener) {
this.onClickBottomListener = onClickBottomListener;
}
public interface OnClickBottomListener {
/**
* 点击确定按钮事件
*/
void onPositiveClick();
/**
* 点击取消按钮事件
*/
void onNegtiveClick();
}
public String getMessage() {
return message;
}
public UpdateDialog setMessage(String message) {
this.message = message;
return this;
}
public String getTitle() {
return title;
}
public UpdateDialog setTitle(String title) {
this.title = title;
return this;
}
public String getPositive() {
return positive;
}
public UpdateDialog setPositive(String positive) {
this.positive = positive;
return this;
}
public int getImageResId() {
return imageResId;
}
public boolean isSingle() {
return isSingle;
}
public UpdateDialog setSingle(boolean single) {
isSingle = single;
return this;
}
public UpdateDialog setImageResId(int imageResId) {
this.imageResId = imageResId;
return this;
}
@Override
public void dismiss() {
super.dismiss();
}
}

View File

@@ -7,10 +7,12 @@ import com.hainaos.vc.BuildConfig;
import com.hainaos.vc.bean.AppInfo;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryUpdateInfo;
import com.hainaos.vc.bean.CodeInfo;
import com.hainaos.vc.bean.LoginInfo;
import com.hainaos.vc.bean.UserInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.bean.uiuios.AppUpdateInfo;
import com.hainaos.vc.config.CommonConfig;
import com.hainaos.vc.network.api.AppApi;
@@ -248,6 +250,31 @@ public class NetInterfaceManager {
.observeOn(AndroidSchedulers.mainThread());
}
@Deprecated
public Observable<BaseResponse<VideoUpdate>> getVideoUpdateObservable(String category_uuid, String current_time) {
String bearerToken = LoginUtils.getInstance().getBearerToken();
return getVideoApi()
.getVideoUpdate(bearerToken, category_uuid, current_time)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Observable<BaseResponse<VideoUpdate>> getVideoUpdateObservable(String current_time) {
String bearerToken = LoginUtils.getInstance().getBearerToken();
return getVideoApi()
.getVideoUpdate(bearerToken, current_time)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Observable<BaseResponse<List<CategoryUpdateInfo>>> getCategoryUpdateObservable(String current_time) {
String bearerToken = LoginUtils.getInstance().getBearerToken();
return getVideoApi()
.categoryUpdateNumber(bearerToken, current_time)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Observable<BaseResponse<AppInfo>> getCheckUpdateObservable() {
return mRetrofit.create(AppApi.class)
.checkUpdate(BuildConfig.APPLICATION_ID)

View File

@@ -22,6 +22,10 @@ public class UrlAddress {
public static final String VIDEO_LIST = "videos/video-list";
/*点击下载后统计上报*/
public static final String VIDEOS_DOWNLOAD = "videos/download";
/*检查视频分类更新*/
public static final String VIDEOS_CHECK_UPDATE = "videos/check-update";
/*检查分类列表是否有视频更新*/
public static final String VIDEOS_CATEGORY_UPDATE = "videos/category-update-number";
/*获取修改密码验证码*/

View File

@@ -2,7 +2,9 @@ package com.hainaos.vc.network.api;
import com.hainaos.vc.bean.BaseResponse;
import com.hainaos.vc.bean.CategoryInfo;
import com.hainaos.vc.bean.CategoryUpdateInfo;
import com.hainaos.vc.bean.VideoListData;
import com.hainaos.vc.bean.VideoUpdate;
import com.hainaos.vc.network.UrlAddress;
import java.util.List;
@@ -34,4 +36,23 @@ public interface VideoApi {
@Header("Authorization") String token,
@Query("uuid") String uuid
);
@GET(UrlAddress.VIDEOS_CHECK_UPDATE)
Observable<BaseResponse<VideoUpdate>> getVideoUpdate(
@Header("Authorization") String token,
@Query("category_uuid") String category_uuid,
@Query("current_time") String current_time
);
@GET(UrlAddress.VIDEOS_CHECK_UPDATE)
Observable<BaseResponse<VideoUpdate>> getVideoUpdate(
@Header("Authorization") String token,
@Query("current_time") String current_time
);
@GET(UrlAddress.VIDEOS_CATEGORY_UPDATE)
Observable<BaseResponse<List<CategoryUpdateInfo>>> categoryUpdateNumber(
@Header("Authorization") String token,
@Query("current_time") String current_time
);
}

View File

@@ -29,4 +29,10 @@ public class TimeUtils {
Date date = new Date(second * 1000);
return sdf.format(date);
}
public static String transferMillisecondToDate(long millisecond) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(millisecond);
return sdf.format(date);
}
}

View File

@@ -2,14 +2,15 @@ package com.hainaos.vc.video;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
@@ -42,132 +43,80 @@ public class AesDataSource2 implements DataSource {
@Override
public long open(DataSpec dataSpec) throws IOException {
// 1. 获取请求的绝对位置
// 1. 先关闭之前的流(如果存在)
close();
final int AES_BLOCK_SIZE = 16;
long position = dataSpec.position;
// AES 块大小通常为 16 字节
final int AES_BLOCK_SIZE = 16;
// 2. 计算块索引 (Block Index) 和 块内偏移 (Offset inside the block)
// 例如position = 100blockIndex = 6 (96字节处)offset = 4
// 2. 计算 CTR 块索引和块内偏移
long blockIndex = position / AES_BLOCK_SIZE;
int offsetInBlock = (int) (position % AES_BLOCK_SIZE);
// 3. 计算对齐后的起始读取位置 (必须是 16 的倍数)
long startPosition = blockIndex * AES_BLOCK_SIZE;
// 3. 构建新的 DataSpec从 16 字节对齐的位置开始读取
// 如果原始请求了长度,我们需要增加 offsetInBlock 以保证能读够对应的数据
long requestLength = dataSpec.length != C.LENGTH_UNSET
? dataSpec.length + offsetInBlock
: C.LENGTH_UNSET;
DataSpec alignedSpec = dataSpec.buildUpon()
.setPosition(startPosition)
.setLength(requestLength)
.build();
// 4. 打开上层数据源并获取实际可读长度
long upstreamLength = upstream.open(alignedSpec);
try {
// 4. 初始化 Cipher
// 5. 初始化 Cipher
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
// 【关键步骤】:根据 blockIndex 计算新的 IV
// CTR 模式下NewIV = OriginalIV + blockIndex
byte[] newIv = getAdjustedIv(this.iv, blockIndex);
IvParameterSpec ivSpec = new IvParameterSpec(newIv);
// 计算调整后的 IV (NewIV = OriginalIV + blockIndex)
byte[] adjustedIv = getAdjustedIv(this.iv, blockIndex);
IvParameterSpec ivSpec = new IvParameterSpec(adjustedIv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 5. 让上层数据源 (FileDataSource) 从对齐的位置 (startPosition) 开始读
// 注意:我们修改了 position但保持 length 不变 (或者处理 open ended)
long length = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length + offsetInBlock : C.LENGTH_UNSET;
// 6. 包装成解密流
// 注意:我们直接包装 upstream 提供的 InputStream
cipherInputStream = new CipherInputStream(new UpstreamInputStream(upstream), cipher);
DataSpec newSpec = dataSpec.buildUpon()
.setPosition(startPosition)
.setLength(length)
.build();
// 打开上层流
InputStream inputStream = new DataSourceInputStream(upstream, newSpec);
// 创建解密流
cipherInputStream = new CipherInputStream(inputStream, cipher);
// 6. 【重要】跳过块内的偏移量
// 因为我们要给 ExoPlayer 返回的是从 dataSpec.position 开始的数据,
// 但我们是从 startPosition (前一个16倍数) 开始解密的,所以前面多读的 offsetInBlock 个字节是无用的。
// 7. 丢弃块内偏移量多出的字节
if (offsetInBlock > 0) {
forceSkip(cipherInputStream, offsetInBlock);
forceSkip(offsetInBlock);
}
opened = true;
// 计算剩余长度
// 8. 计算返回给 ExoPlayer 的剩余长度
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else if (upstreamLength != C.LENGTH_UNSET) {
bytesRemaining = upstreamLength - offsetInBlock;
} else {
long upstreamLength = upstream.open(newSpec); // 这一步其实已经在 DataSourceInputStream 里做过了,这里仅作逻辑参考
// 通常 upstream.open 返回的是从 startPosition 开始的长度
// 如果 upstream 支持长度解析:
// bytesRemaining = upstreamLength != C.LENGTH_UNSET ? upstreamLength - offsetInBlock : C.LENGTH_UNSET;
bytesRemaining = C.LENGTH_UNSET;
}
opened = true;
return bytesRemaining;
} catch (Exception e) {
throw new IOException(e);
}
}
/**
* 根据块索引计算新的 IV。
* CTR 模式将 IV 视为一个大整数 (BigEndian),每过一个块,计数器 +1。
*/
private byte[] getAdjustedIv(byte[] originalIv, long blockIndex) {
// 使用 BigInteger 处理大数加法,防止溢出
BigInteger ivVal = new BigInteger(1, originalIv);
BigInteger offset = BigInteger.valueOf(blockIndex);
BigInteger newIvVal = ivVal.add(offset);
byte[] newIv = newIvVal.toByteArray();
// BigInteger.toByteArray() 可能会因为符号位导致长度变为 17 (如果是正数且最高位是1)
// 或者因为数值较小导致长度小于 16。必须确保返回 16 字节。
byte[] result = new byte[16];
int srcOffset = newIv.length > 16 ? newIv.length - 16 : 0;
int dstOffset = newIv.length < 16 ? 16 - newIv.length : 0;
int copyLength = newIv.length > 16 ? 16 : newIv.length;
System.arraycopy(newIv, srcOffset, result, dstOffset, copyLength);
return result;
}
/**
* 强制跳过指定字节数。CipherInputStream 的 skip 有时不可靠,建议循环 read。
*/
private void forceSkip(InputStream stream, int bytesToSkip) throws IOException {
long skipped = 0;
byte[] skipBuffer = new byte[1024];
while (skipped < bytesToSkip) {
int toRead = (int) Math.min(bytesToSkip - skipped, skipBuffer.length);
int read = stream.read(skipBuffer, 0, toRead);
if (read == -1) {
break;
}
skipped += read;
throw new IOException("Failed to initialize AES cipher", e);
}
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
if (readLength == 0) {
return 0;
}
int bytesToRead = readLength;
if (bytesRemaining != C.LENGTH_UNSET) {
bytesToRead = (int) Math.min(readLength, bytesRemaining);
}
if (bytesToRead == 0) {
return C.RESULT_END_OF_INPUT;
}
if (readLength == 0) return 0;
// 限制读取长度,不要超过 bytesRemaining
int bytesToRead = (bytesRemaining == C.LENGTH_UNSET)
? readLength
: (int) Math.min(readLength, bytesRemaining);
if (bytesToRead <= 0 && bytesRemaining != C.LENGTH_UNSET) return C.RESULT_END_OF_INPUT;
int bytesRead = cipherInputStream.read(buffer, offset, bytesToRead);
if (bytesRead == -1) {
if (bytesRemaining != C.LENGTH_UNSET) {
// 预期还要读数据但读不到了,抛错
throw new IOException("End of stream reached prematurely");
}
return C.RESULT_END_OF_INPUT;
}
@@ -177,6 +126,41 @@ public class AesDataSource2 implements DataSource {
return bytesRead;
}
private void forceSkip(int bytesToSkip) throws IOException {
int skipped = 0;
byte[] skipBuffer = new byte[Math.min(bytesToSkip, 2048)];
while (skipped < bytesToSkip) {
int toRead = Math.min(bytesToSkip - skipped, skipBuffer.length);
int read = cipherInputStream.read(skipBuffer, 0, toRead);
if (read == -1) throw new EOFException("Check your offset and file size.");
skipped += read;
}
}
private byte[] getAdjustedIv(byte[] originalIv, long blockIndex) {
if (blockIndex == 0) return originalIv;
BigInteger ivVal = new BigInteger(1, originalIv);
BigInteger newIvVal = ivVal.add(BigInteger.valueOf(blockIndex));
byte[] raw = newIvVal.toByteArray();
byte[] result = new byte[16];
int length = Math.min(raw.length, 16);
System.arraycopy(raw, Math.max(0, raw.length - 16), result, 16 - length, length);
return result;
}
@Override
public void close() throws IOException {
if (opened) {
opened = false;
if (cipherInputStream != null) {
cipherInputStream.close();
cipherInputStream = null;
}
upstream.close();
}
}
@Nullable
@Override
public Uri getUri() {
return upstream.getUri();
@@ -187,14 +171,25 @@ public class AesDataSource2 implements DataSource {
return upstream.getResponseHeaders();
}
@Override
public void close() throws IOException {
if (opened) {
opened = false;
if (cipherInputStream != null) {
cipherInputStream.close(); // 这也会关闭 upstream
cipherInputStream = null;
}
/**
* 一个简单的内部类,将 DataSource 转为 InputStream
*/
private static final class UpstreamInputStream extends java.io.InputStream {
private final DataSource dataSource;
private final byte[] singleByteArray = new byte[1];
public UpstreamInputStream(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public int read() throws IOException {
return read(singleByteArray) == -1 ? -1 : (singleByteArray[0] & 0xFF);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return dataSource.read(b, off, len);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="41.4dp"
android:viewportWidth="100"
android:viewportHeight="41.4">
<path
android:pathData="M19.4,19.9h11.7v2.7h-11.7v-2.7Z"
android:fillColor="#d81e06" />
<path
android:pathData="M79.9,0.8H20C9,0.8 0,9.8 0,20.9v16.7c0,2 1.4,3.4 3.4,3.4h76.6c11,0 20.1,-9 20.1,-20.1S91,0.8 79.9,0.8ZM61,13.7h5c-0.3,-0.7 -0.7,-1.3 -1.2,-2.1l1.5,-0.7c0.5,0.8 1,1.6 1.3,2.4l-0.8,0.4h4.9v1.5h-10.9s0,-1.5 0,-1.5ZM60.3,19.1h3.8c-0.7,-1.1 -1.3,-1.9 -1.9,-2.7l1.2,-0.9c0.5,0.6 1.2,1.5 2,2.6l-1.3,0.9h3c0.8,-1.2 1.6,-2.4 2.4,-3.6l1.6,0.9c-0.7,0.9 -1.5,1.9 -2.1,2.7h3.5v1.5h-12.2s0,-1.5 0,-1.4ZM35.7,15.6h-13.9c-0.5,0.9 -1,1.8 -1.6,2.6h12.7v12.4c0,1.9 -0.9,2.9 -2.8,2.9h-3.7c0,-0.6 -0.2,-1.2 -0.4,-1.9 1.4,0 2.5,0.2 3.5,0.2s1.6,-0.6 1.6,-1.6v-1.6h-11.6v5.3h-1.9v-12c-1.1,1.3 -2.3,2.5 -3.7,3.8 -0.3,-0.6 -0.7,-1.1 -1.1,-1.7 3,-2.5 5.3,-5.3 7,-8.3h-6.4v-1.7h7.2c0.5,-0.9 0.8,-1.9 1.2,-3l2,0.5c-0.3,0.9 -0.7,1.7 -1,2.5h13.1s0,1.7 0,1.7ZM58.7,33.4c-6.2,-0.2 -10.6,-0.9 -13.6,-2.2 -1.8,1.2 -4.3,2.1 -7.6,2.9 -0.3,-0.6 -0.7,-1.1 -1.1,-1.6 2.8,-0.4 5.1,-1.2 6.8,-2.2 -1.3,-0.9 -2.5,-2.1 -3.6,-3.6l1.2,-1h-1.8v-9.7h8.1v-2.1h-10v-1.6h21.3v1.6h-9.5v2.1h7.7v10.8h-1.7v-1.2h-6.5c-0.4,1.9 -1.1,3.2 -1.9,4.3 3.1,1.2 7.4,1.7 12.9,1.7 -0.3,0.7 -0.7,1.2 -1,1.8h0ZM60.3,30.6c0.7,-1.2 1.6,-2.7 2.5,-4.7l1.6,0.7c-0.8,1.7 -1.6,3.3 -2.5,4.8 -0.4,-0.3 -1,-0.6 -1.6,-0.8ZM67.4,31c0,1.6 -0.8,2.5 -2.4,2.5h-2.2c0,-0.5 -0.2,-1.1 -0.2,-1.7 0.9,0 1.6,0.2 2.1,0.2 0.7,0 1.1,-0.4 1.1,-1.2v-5.7h-4.8v-1.5h4.8v-2.2h1.6v2.2h4.5v1.5h-4.5v6ZM68.1,27l1.3,-0.9c0.8,1.2 1.6,2.2 2.2,3.3l-1.5,1c-0.6,-1.2 -1.3,-2.3 -2.1,-3.4ZM83.5,21.6h-3.3v12.2h-1.7v-12.2h-3.5v1.2c0,4.8 -1.2,8.5 -3.4,11.1 -0.3,-0.5 -0.8,-1 -1.2,-1.5 2,-2.2 3,-5.4 3,-9.6v-9.7c3.2,0 6.2,-0.4 8.9,-0.9l0.6,1.8c-3,0.4 -5.7,0.7 -7.8,0.8v5.4h8.5v1.6Z"
android:fillColor="#d81e06" />
<path
android:pathData="M19.4,24.2h11.7v2.8h-11.7v-2.8ZM41.2,25.7c1.1,1.5 2.3,2.6 3.9,3.5 0.9,-0.7 1.4,-1.8 1.9,-3.5h-5.7ZM55.2,17.5h-5.9c0,0.9 0,1.8 0,2.5h6v-2.5ZM47.4,21.6h-6.4v2.5h6.1c0.2,-0.7 0.2,-1.6 0.3,-2.5ZM47.5,17.5h-6.4v2.5h6.3c0,-0.7 0,-1.6 0,-2.5ZM55.2,21.6h-6c0,0.9 0,1.7 -0.2,2.5h6.3v-2.5h0Z"
android:fillColor="#d81e06" />
</vector>

View File

@@ -63,6 +63,19 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="海纳美业学习机" />
<ImageView
android:id="@+id/iv_update"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_video_update"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -76,39 +89,39 @@
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent">-->
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_video"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_video"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_nodata"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_nodata"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:singleLine="true"
android:text="没有数据"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:singleLine="true"
android:text="没有数据"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>-->
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -70,16 +70,30 @@
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginEnd="8dp"
android:text="视频:0个"
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_update"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/video_total" />
<ImageView
android:id="@+id/iv_update"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_video_update"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@@ -0,0 +1,90 @@
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="240dp"
android:layout_height="160dp"
android:layout_centerInParent="true"
android:background="@drawable/bg_dialog"
android:minWidth="240dp"
android:orientation="vertical"
android:paddingTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/black"
android:textSize="12sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="消息提示" />
</LinearLayout>
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:gravity="center"
android:lineSpacingExtra="3dp"
android:lineSpacingMultiplier="1.2"
android:minHeight="50dp"
android:textColor="@color/gray"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="提示消息提示消息提示消息提示消息提示消息" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/positive"
android:layout_width="48dp"
android:layout_height="20dp"
android:layout_weight="1"
android:background="@drawable/bg_login_button"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="8sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:text="确定" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -28,6 +28,18 @@
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/icon_category" />
<ImageView
android:id="@+id/iv_update"
android:layout_width="20dp"
android:layout_height="8dp"
android:layout_marginEnd="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_video_update"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -72,9 +84,9 @@
android:maxLines="2"
android:textColor="@color/white"
android:textSize="8sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>