diff --git a/.github/workflows/.github-ci.yml b/.github/workflows/.github-ci.yml index ea23b49c..60049fb9 100644 --- a/.github/workflows/.github-ci.yml +++ b/.github/workflows/.github-ci.yml @@ -49,7 +49,7 @@ jobs: distribution: 'zulu' # 指定 Java 发行版 - name: Set up Android SDK - uses: android-actions/setup-android@v2 + uses: android-actions/setup-android@v4 with: api-level: 30 build-tools: 30.0.3 diff --git a/pos_android_studio_demo/pos_android_app/build.gradle b/pos_android_studio_demo/pos_android_app/build.gradle index 0ea2ca5a..471e749f 100644 --- a/pos_android_studio_demo/pos_android_app/build.gradle +++ b/pos_android_studio_demo/pos_android_app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "com.dspread.pos_android_app" minSdk 24 targetSdk 34 - versionCode 148 - versionName "8.5.5-2609101405" + versionCode 149 + versionName "8.5.5-2609201905" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { // Set up supported SO library architecture diff --git a/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5-min.aar b/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5-min.aar index e808cb95..b03ffd5e 100644 Binary files a/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5-min.aar and b/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5-min.aar differ diff --git a/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5.aar b/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5.aar index d7de0efd..22f3c506 100644 Binary files a/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5.aar and b/pos_android_studio_demo/pos_android_app/libs/dspread_pos_sdk_8.5.5.aar differ diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/Constant.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/Constant.java new file mode 100644 index 00000000..bd41c923 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/Constant.java @@ -0,0 +1,17 @@ +package com.dspread.pos.dualScreen.manager; + +public class Constant { + public static final String VERSION = "1.0.0"; ///< 版本号 + + // ===== 返回码常量 ===== + public class ResultConstant { + public static final int RESULT_SUCCESS = 0; + public static final int RESULT_ERROR_NO_DISPLAY = -1; + public static final int RESULT_ERROR_INVALID_PARAM = -2; + public static final int RESULT_ERROR_FILE_NOT_FOUND = -3; + public static final int RESULT_ERROR_NOT_SUPPORTED = -4; + public static final int RESULT_ERROR_SHOW_FAILED = -5; + } +} + + diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IResultCallback.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IResultCallback.java new file mode 100644 index 00000000..849edc04 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IResultCallback.java @@ -0,0 +1,10 @@ +package com.dspread.pos.dualScreen.manager; + +/** + * 操作结果回调接口 + */ +public interface IResultCallback { + void onSuccess(); + + void onFailure(int code, String msg); +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IViceScreen.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IViceScreen.java new file mode 100644 index 00000000..4f75c12d --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/IViceScreen.java @@ -0,0 +1,53 @@ +package com.dspread.pos.dualScreen.manager; + +import android.view.View; + +import java.util.ArrayList; + +/** + * 副屏控制接口 + * + *

定义了副屏图片显示、视频播放、自定义View显示、 + * 电源控制、亮度调节、开机Logo、分辨率查询等能力。

+ */ +public interface IViceScreen { + + /** 显示单张图片 */ + int showPic(String picPath); + + /** 显示图片轮播 */ + int showPic(ArrayList picPathList, int intervalSeconds); + + /** 播放视频 */ + int showVideo(String videoPath); + + /** 显示自定义View */ + void show(View view, IResultCallback callback); + + /** 显示自定义View + 图片轮播 */ + void show(View view, ArrayList picDataList, IResultCallback callback); + + /** 显示壁纸 */ + int showWallpaper(); + + /** 电源开关 */ + int power(boolean on); + + /** 获取电源状态 */ + int getPowerOnStatus(); + + /** 设置亮度 */ + int setBrightness(int value); + + /** 获取亮度 */ + int getBrightness(); + + /** 设置开机Logo */ + int setBootLogo(String filePath); + + /** 获取副屏分辨率 [width, height] */ + int[] getScreenResolution(); + + /** 获取副屏描述信息*/ + String getScreenDescription(); +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/PictureData.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/PictureData.java new file mode 100644 index 00000000..a441ada2 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/PictureData.java @@ -0,0 +1,102 @@ +package com.dspread.pos.dualScreen.manager; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * 图片显示数据 + * + *

描述一张图片在副屏上的显示位置和尺寸。

+ */ +public class PictureData implements Parcelable { + private String path; + private int startX; + private int startY; + private int width; + private int height; + + public PictureData() { + } + + public PictureData(String path, int startX, int startY, int width, int height) { + this.path = path; + this.startX = startX; + this.startY = startY; + this.width = width; + this.height = height; + } + + protected PictureData(Parcel in) { + path = in.readString(); + startX = in.readInt(); + startY = in.readInt(); + width = in.readInt(); + height = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PictureData createFromParcel(Parcel in) { + return new PictureData(in); + } + + @Override + public PictureData[] newArray(int size) { + return new PictureData[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(path); + dest.writeInt(startX); + dest.writeInt(startY); + dest.writeInt(width); + dest.writeInt(height); + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public int getStartX() { + return startX; + } + + public void setStartX(int startX) { + this.startX = startX; + } + + public int getStartY() { + return startY; + } + + public void setStartY(int startY) { + this.startY = startY; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/ViceScreenManager.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/ViceScreenManager.java new file mode 100644 index 00000000..36408103 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/manager/ViceScreenManager.java @@ -0,0 +1,115 @@ +package com.dspread.pos.dualScreen.manager; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; + + +import com.dspread.pos.dualScreen.original.ViceScreenOriginal; +import com.dspread.pos_android_app.R; + +import java.util.ArrayList; + +public class ViceScreenManager { + private static volatile ViceScreenManager instance; + + private Context context; + private int deviceType; + private ViceScreenOriginal original; + + private ViceScreenManager(Context context, int deviceType) { + this.context = context.getApplicationContext(); + this.deviceType = deviceType; + this.original = new ViceScreenOriginal(context); + } + + /** + * 获取全局单例(默认设备类型) + * 可在任意 Activity 或其他页面直接调用 + */ + public static ViceScreenManager getInstance(Context context) { + return getInstance(context, 0); + } + + /** + * 获取全局单例(指定设备类型) + */ + public static ViceScreenManager getInstance(Context context, int deviceType) { + if (instance == null) { + synchronized (ViceScreenManager.class) { + if (instance == null) { + instance = new ViceScreenManager(context, deviceType); + } + } + } + return instance; + } + + public int showPic(String picPath) { + return original.showPic(picPath); + } + + public int showPic(ArrayList picPathList, int intervalSeconds) { + return original.showPic(picPathList, intervalSeconds); + } + + public int showVideo(String videoPath) { + return original.showVideo(videoPath); + } + + public void show(View view) { + show(view, null); + } + + public void show(View view, IResultCallback callback) { + original.show(view, callback); + } + + /** + * 在副屏上显示默认欢迎页(副屏已开启时) + * 供任意页面复用,内部自动判断副屏电源状态 + */ + public void showDefaultView() { + if (getPowerOnStatus() == 1) { + LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.vice_default, null); + show(view, null); + } + } + + public void show(View view, ArrayList picDataList, IResultCallback callback) { + original.show(view, picDataList, callback); + } + + public int showWallpaper() { + return original.showWallpaper(); + } + + public int power(boolean on) { + return original.power(on); + } + + public int getPowerOnStatus() { + return original.getPowerOnStatus(); + } + + public int setBrightness(int value) { + return original.setBrightness(value); + } + + public int getBrightness() { + return original.getBrightness(); + } + + public int setBootLogo(String filePath) { + return original.setBootLogo(filePath); + } + + public int[] getScreenResolution() { + return original.getScreenResolution(); + } + + public String getScreenDescription() { + return original.getScreenDescription(); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/PicturePresentation.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/PicturePresentation.java new file mode 100644 index 00000000..6a490147 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/PicturePresentation.java @@ -0,0 +1,201 @@ +package com.dspread.pos.dualScreen.original; + +import android.app.Presentation; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Display; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; + + +import com.dspread.pos.dualScreen.manager.PictureData; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 图片显示 Presentation(原生实现) + * + *

支持三种模式: + *

    + *
  • 单张图片全屏显示
  • + *
  • 多张图片轮播(可设定间隔秒数)
  • + *
  • 多张图片按指定位置和尺寸叠加显示
  • + *
+ *

+ */ +public class PicturePresentation extends Presentation { + + private final FrameLayout rootLayout; + private final ImageView fullImageView; + private final Handler handler = new Handler(Looper.getMainLooper()); + + // 轮播相关 + private List picPathList; + private int intervalMillis; + private int currentIndex = 0; + private boolean isStopSlide = false; + + private final Runnable slideshowRunnable = new Runnable() { + @Override + public void run() { + if (isStopSlide || picPathList == null || picPathList.isEmpty()) return; + showImage(picPathList.get(currentIndex), 0, 0, 0, 0, true); + currentIndex = (currentIndex + 1) % picPathList.size(); + handler.postDelayed(this, intervalMillis); + } + }; + + public PicturePresentation(Context context, Display display) { + super(context, display); + rootLayout = new FrameLayout(getContext()); + rootLayout.setBackgroundColor(Color.BLACK); + rootLayout.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + fullImageView = new ImageView(getContext()); + fullImageView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + fullImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + rootLayout.addView(fullImageView); + setContentView(rootLayout); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setupCutoutMode(); + } + + @Override + public void hide() { + super.hide(); + isStopSlide = true; + } + + /** 让窗口延伸到挖孔(cutout)区域,占满整个副屏 */ + private void setupCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + window.setAttributes(attrs); + } + } + } + + /** + * 显示单张图片(全屏) + */ + public void showSinglePic(String picPath) { + stopSlideshow(); + showImage(picPath, 0, 0, 0, 0, true); + } + + /** + * 显示图片轮播 + * @param paths 图片路径列表 + * @param intervalSeconds 轮播间隔(秒) + */ + public void showSlideshow(List paths, int intervalSeconds) { + stopSlideshow(); + if (paths == null || paths.isEmpty()) return; + this.picPathList = new ArrayList<>(paths); + this.intervalMillis = Math.max(1, intervalSeconds) * 1000; + this.currentIndex = 0; + this.isStopSlide = false; + handler.post(slideshowRunnable); + } + + /** + * 显示多张图片按指定位置和尺寸叠加 + */ + public void showPicDataList(List picDataList) { + stopSlideshow(); + // 清除全屏 ImageView,改为按位置叠加 + fullImageView.setVisibility(android.view.View.GONE); + rootLayout.setBackgroundColor(Color.BLACK); + // 清除之前按位置添加的 ImageView(保留 fullImageView) + while (rootLayout.getChildCount() > 1) { + rootLayout.removeViewAt(rootLayout.getChildCount() - 1); + } + if (picDataList == null) return; + for (PictureData pd : picDataList) { + ImageView iv = new ImageView(getContext()); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + pd.getWidth() > 0 ? pd.getWidth() : FrameLayout.LayoutParams.WRAP_CONTENT, + pd.getHeight() > 0 ? pd.getHeight() : FrameLayout.LayoutParams.WRAP_CONTENT); + lp.leftMargin = pd.getStartX(); + lp.topMargin = pd.getStartY(); + iv.setLayoutParams(lp); + iv.setScaleType(ImageView.ScaleType.FIT_XY); + loadImage(pd.getPath(), iv); + rootLayout.addView(iv); + } + } + + @Override + protected void onStop() { + super.onStop(); + stopSlideshow(); + } + + private void stopSlideshow() { + handler.removeCallbacks(slideshowRunnable); + picPathList = null; + isStopSlide = true; + } + + /** + * 加载并显示图片 + * @param path 图片文件路径 + * @param x 起始X(0=居中全屏) + * @param y 起始Y + * @param w 宽度(0=match_parent) + * @param h 高度(0=match_parent) + * @param fullscreen 是否全屏模式 + */ + private void showImage(String path, int x, int y, int w, int h, boolean fullscreen) { + if (fullscreen) { + fullImageView.setVisibility(android.view.View.VISIBLE); + loadImage(path, fullImageView); + } + } + + /** + * 加载图片到指定 ImageView + */ + private void loadImage(String path, ImageView target) { + if (path == null || path.isEmpty()) return; + File file = new File(path); + if (!file.exists()) return; + try { + // 使用 inSampleSize 控制内存,避免大图 OOM + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, opts); + int sampleSize = Math.max(1, opts.outWidth / 960); + opts.inJustDecodeBounds = false; + opts.inSampleSize = sampleSize; + opts.inPreferredConfig = Bitmap.Config.RGB_565; + Bitmap bmp = BitmapFactory.decodeFile(path, opts); + if (bmp != null) { + target.setImageBitmap(bmp); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViceScreenOriginal.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViceScreenOriginal.java new file mode 100644 index 00000000..d0963b83 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViceScreenOriginal.java @@ -0,0 +1,419 @@ +package com.dspread.pos.dualScreen.original; + +import android.app.Presentation; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.View; + + +import com.dspread.pos.dualScreen.manager.Constant; +import com.dspread.pos.dualScreen.manager.IResultCallback; +import com.dspread.pos.dualScreen.manager.IViceScreen; +import com.dspread.pos.dualScreen.manager.PictureData; +import com.dspread.pos_android_app.R; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class ViceScreenOriginal implements IViceScreen { + /** + * 副屏连接状态变化监听器 + */ + public interface OnDisplayChangedListener { + void onDisplayChanged(boolean connected, Display display); + } + + private final Context context; + private final DisplayManager displayManager; + private PicturePresentation picturePresentation; + private VideoPresentation videoPresentation; + private ViewPresentation viewPresentation; + private int intervalSeconds; + + public ViceScreenOriginal(Context context) { + this.context = context.getApplicationContext(); + this.displayManager = (DisplayManager) this.context.getSystemService(Context.DISPLAY_SERVICE); + this.intervalSeconds = 3; + createPresentation(); + } + + /** + * 检测副屏是否已连接 + */ + public boolean hasSecondaryDisplay() { + return getSecondaryDisplay() != null; + } + + /** + * 获取副屏 Display 对象 + */ + private Display getSecondaryDisplay() { + if (displayManager == null) return null; + Display[] displays = displayManager.getDisplays(); + if (displays == null) return null; + // 跳过主屏(displayId=0),取第一个非主屏 + for (Display d : displays) { + if (d.getDisplayId() != Display.DEFAULT_DISPLAY) { + return d; + } + } + return null; + } + + + // ===== 副屏参数描述 ===== + + /** + * 生成副屏的多行参数描述(分辨率/刷新率/DPI/方向/类型等) + */ + public String describeSecondaryDisplay() { + Display display = getSecondaryDisplay(); + if (display == null) return context.getString(R.string.secondary_display_not_connected); + + StringBuilder sb = new StringBuilder(); + DisplayMetrics real = new DisplayMetrics(); + display.getRealMetrics(real); + sb.append(context.getString(R.string.display_desc_title, + display.getDisplayId(), real.widthPixels, real.heightPixels)); + + Display.Mode mode = display.getMode(); + if (mode != null) { + sb.append("\n").append(context.getString(R.string.display_refresh_rate, mode.getRefreshRate())); + } + sb.append(String.format(Locale.getDefault(), " · DPI: %.0f", real.xdpi)); + sb.append(" · ").append(real.widthPixels >= real.heightPixels + ? context.getString(R.string.orientation_landscape) + : context.getString(R.string.orientation_portrait)); + + sb.append("\n").append(context.getString(R.string.display_type, flagsToString(display.getFlags()))); + sb.append("\n").append(context.getString(R.string.display_touch, + (display.getFlags() & F_SUPPORTS_TOUCH) != 0 + ? context.getString(R.string.touch_supported) + : context.getString(R.string.touch_not_reported))); + + Display.Mode[] modes = display.getSupportedModes(); + if (modes != null && modes.length > 0) { + sb.append("\n").append(context.getString(R.string.display_supported_modes, + modes.length, real.widthPixels, real.heightPixels, + mode != null ? mode.getRefreshRate() : 0)); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + String name = display.getName(); + if (name != null && !name.isEmpty()) { + sb.append("\n").append(context.getString(R.string.display_name, name)); + } + } + return sb.toString(); + } + + // Display flags 位掩码(硬编码数值避免 compileSdk 版本差异) + private static final int F_SECURE = 0x00000002; // Display.FLAG_SECURE (1<<1) + private static final int F_PRESENTATION = 0x00000008; // Display.FLAG_PRESENTATION (1<<3) + private static final int F_ROUND = 0x00000010; // Display.FLAG_ROUND (1<<4) + private static final int F_SUPPORTS_TOUCH = 0x00000020; // Display.FLAG_SUPPORTS_TOUCH (1<<5, @hide 需硬编码) + private static final int F_NOT_FOCUSABLE = 0x00000200; // 1<<9 + + /** + * 把 Display flags 转换成可读字符串。 + */ + private String flagsToString(int flags) { + List parts = new ArrayList<>(); + if ((flags & F_SECURE) != 0) parts.add(context.getString(R.string.flag_secure)); + if ((flags & F_PRESENTATION) != 0) parts.add(context.getString(R.string.flag_presentation)); + if ((flags & F_ROUND) != 0) parts.add(context.getString(R.string.flag_round)); + if ((flags & F_SUPPORTS_TOUCH) != 0) parts.add(context.getString(R.string.flag_touch)); + if ((flags & F_NOT_FOCUSABLE) != 0) + parts.add(context.getString(R.string.flag_not_focusable)); + if (parts.isEmpty()) parts.add(context.getString(R.string.flag_default)); + return String.join(" / ", parts); + } + + @Override + public int showPic(String picPath) { + if (picPath == null || picPath.isEmpty()) { + return Constant.ResultConstant.RESULT_ERROR_INVALID_PARAM; + } + if (!new File(picPath).exists()) { + return Constant.ResultConstant.RESULT_ERROR_FILE_NOT_FOUND; + } + if (picturePresentation == null) { + Display display = getSecondaryDisplay(); + if (display == null) return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + picturePresentation = new PicturePresentation(context, display); + } + if (picturePresentation == null) { + return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + } + + try { + showPresentation(picturePresentation); + picturePresentation.showSinglePic(picPath); + return Constant.ResultConstant.RESULT_SUCCESS; + } catch (Exception e) { + return Constant.ResultConstant.RESULT_ERROR_SHOW_FAILED; + } + } + + @Override + public int showPic(ArrayList picPathList, int intervalSeconds) { + if (picPathList == null || picPathList.isEmpty()) { + return Constant.ResultConstant.RESULT_ERROR_INVALID_PARAM; + } + // 验证文件存在性 + for (String path : picPathList) { + if (path == null || !new File(path).exists()) { + return Constant.ResultConstant.RESULT_ERROR_FILE_NOT_FOUND; + } + } + if (picturePresentation == null) { + Display display = getSecondaryDisplay(); + if (display == null) return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + picturePresentation = new PicturePresentation(context, display); + } + if (picturePresentation == null) { + return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + } + + try { + showPresentation(picturePresentation); + picturePresentation.showSlideshow(picPathList, intervalSeconds); + this.intervalSeconds = intervalSeconds; + return Constant.ResultConstant.RESULT_SUCCESS; + } catch (Exception e) { + return Constant.ResultConstant.RESULT_ERROR_SHOW_FAILED; + } + } + + @Override + public int showVideo(String videoPath) { + if (videoPath == null || videoPath.isEmpty()) { + return Constant.ResultConstant.RESULT_ERROR_INVALID_PARAM; + } + if (!new File(videoPath).exists()) { + return Constant.ResultConstant.RESULT_ERROR_FILE_NOT_FOUND; + } + if (videoPresentation == null) { + Display display = getSecondaryDisplay(); + if (display == null) return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + videoPresentation = new VideoPresentation(context, display); + } + if (videoPresentation == null) { + return Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY; + } + + try { + showPresentation(videoPresentation); + videoPresentation.playVideo(videoPath); + return Constant.ResultConstant.RESULT_SUCCESS; + } catch (Exception e) { + return Constant.ResultConstant.RESULT_ERROR_SHOW_FAILED; + } + } + + @Override + public void show(View view, IResultCallback callback) { + if (viewPresentation == null) { + Display display = getSecondaryDisplay(); + if (display == null) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY, context.getString(R.string.secondary_display_not_connected)); + return; + } + viewPresentation = new ViewPresentation(context, display); + } + if (viewPresentation == null) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY, context.getString(R.string.secondary_display_not_connected)); + return; + } + + try { + showPresentation(viewPresentation); + viewPresentation.showView(view); + if (callback != null) callback.onSuccess(); + } catch (Exception e) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_SHOW_FAILED, e.getMessage()); + } + } + + @Override + public void show(View view, ArrayList picDataList, IResultCallback callback) { + if (viewPresentation == null) { + Display display = getSecondaryDisplay(); + if (display == null) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY, context.getString(R.string.secondary_display_not_connected)); + return; + } + viewPresentation = new ViewPresentation(context, display); + } + if (viewPresentation == null) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_NO_DISPLAY, context.getString(R.string.secondary_display_not_connected)); + return; + } + + try { + showPresentation(viewPresentation); + viewPresentation.showViewWithPics(view, picDataList, intervalSeconds); + if (callback != null) callback.onSuccess(); + } catch (Exception e) { + if (callback != null) + callback.onFailure(Constant.ResultConstant.RESULT_ERROR_SHOW_FAILED, e.getMessage()); + } + } + + @Override + public int showWallpaper() { + return Constant.ResultConstant.RESULT_ERROR_NOT_SUPPORTED; + /* + Display display = getSecondaryDisplay(); + if (display == null) return ResultConstant.RESULT_ERROR_NO_DISPLAY; + + dismissCurrent(); + try { + // 显示纯黑壁纸背景 + Presentation p = new Presentation(context, display) { + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FrameLayout root = new FrameLayout(getContext()); + root.setBackgroundColor(Color.BLACK); + root.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + setContentView(root); + } + }; + p.show(); + currentPresentation = p; + return ResultConstant.RESULT_SUCCESS; + } catch (Exception e) { + return ResultConstant.RESULT_ERROR_SHOW_FAILED; + }*/ + } + + @Override + public int power(boolean on) { + if (on) { + createPresentation(); + } else { + dismissPresentation(); + } + return hasSecondaryDisplay() ? Constant.ResultConstant.RESULT_SUCCESS : Constant.ResultConstant.RESULT_ERROR_NOT_SUPPORTED; + } + + @Override + public int getPowerOnStatus() { + return hasSecondaryDisplay() ? 1 : 0; + } + + @Override + public int setBrightness(int value) { + // Android Presentation API 无法控制副屏亮度,需硬件厂商SDK支持 + return Constant.ResultConstant.RESULT_ERROR_NOT_SUPPORTED; + } + + @Override + public int getBrightness() { + // Android Presentation API 无法控制副屏亮度,需硬件厂商SDK支持 + return Constant.ResultConstant.RESULT_ERROR_NOT_SUPPORTED; + } + + @Override + public int setBootLogo(String filePath) { + // Android Presentation API 无法设置开机Logo,需硬件厂商SDK支持 + return Constant.ResultConstant.RESULT_ERROR_NOT_SUPPORTED; + } + + @Override + public int[] getScreenResolution() { + Display display = getSecondaryDisplay(); + if (display == null) { + return new int[]{0, 0}; + } + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + return new int[]{metrics.widthPixels, metrics.heightPixels}; + } + + @Override + public String getScreenDescription() { + return describeSecondaryDisplay(); + } + + /** + * 切换 Presentation + */ + private void showPresentation(Presentation presentation) { + if (presentation instanceof PicturePresentation) { + picturePresentation.show(); + } else { + picturePresentation.hide(); + } + if (presentation instanceof VideoPresentation) { + videoPresentation.show(); + } else { + videoPresentation.hide(); + } + if (presentation instanceof ViewPresentation) { + viewPresentation.show(); + } else { + viewPresentation.hide(); + } + } + + /** + * 新建 Presentation + */ + private void createPresentation() { + Display display = getSecondaryDisplay(); + if (display == null) return; + if (picturePresentation == null) { + picturePresentation = new PicturePresentation(context, display); + } + if (videoPresentation == null) { + videoPresentation = new VideoPresentation(context, display); + } + if (viewPresentation == null) { + viewPresentation = new ViewPresentation(context, display); + } + } + + /** + * 关闭 Presentation + */ + private void dismissPresentation() { + if (picturePresentation != null) { + try { + picturePresentation.hide(); + picturePresentation.dismiss(); + } catch (Exception ignored) { + } + picturePresentation = null; + } + if (videoPresentation != null) { + try { + videoPresentation.hide(); + videoPresentation.dismiss(); + } catch (Exception ignored) { + } + videoPresentation = null; + } + if (viewPresentation != null) { + try { + viewPresentation.hide(); + viewPresentation.dismiss(); + } catch (Exception ignored) { + } + viewPresentation = null; + } + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/VideoPresentation.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/VideoPresentation.java new file mode 100644 index 00000000..8bd7512c --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/VideoPresentation.java @@ -0,0 +1,192 @@ +package com.dspread.pos.dualScreen.original; + +import android.app.Presentation; +import android.content.Context; +import android.graphics.Color; +import android.graphics.SurfaceTexture; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Display; +import android.view.Surface; +import android.view.TextureView; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +/** + * 视频显示 Presentation(原生实现)。 + * + *

方案1:用 TextureView + MediaPlayer(走 GLES 合成)替代 VideoView/SurfaceView(走 overlay + * 合成),规避 MTK hwcomposer 长时间合成副屏视频图层时的 vendor 崩溃。

+ * + *

方案2:定时重启播放(周期 stop+release 再重新 prepare),释放图层 buffer, + * 避免长时间连续循环合成累积出错。

+ */ +public class VideoPresentation extends Presentation { + + private TextureView textureView; + private MediaPlayer mediaPlayer; + private String videoPath; + private Surface surface; + + /* + private final Handler handler = new Handler(Looper.getMainLooper()); + // 定时重启间隔(毫秒),避免长时间连续合成触发 HWC 崩溃 + private static final long RESTART_INTERVAL_MS = 5 * 60 * 1000L; + + private final Runnable restartRunnable = this::restartPlayback;*/ + + public VideoPresentation(Context context, Display display) { + super(context, display); + FrameLayout root = new FrameLayout(getContext()); + root.setBackgroundColor(Color.BLACK); + root.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + textureView = new TextureView(getContext()); + textureView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + root.addView(textureView); + setContentView(root); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // window 在 show() 之后才真正创建,此时设置 cutout 模式才可靠 + setupCutoutMode(); + } + + @Override + public void hide() { + super.hide(); + if (surface != null && mediaPlayer != null) { + mediaPlayer.pause(); + } + } + + /** + * 让窗口延伸到挖孔(cutout)区域,占满整个副屏 + */ + private void setupCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + window.setAttributes(attrs); + } + } + } + + /** + * 播放指定路径的视频 + */ + public void playVideo(String videoPath) { + if (videoPath == null || videoPath.isEmpty()) return; + if (videoPath.equals(this.videoPath) && surface != null && mediaPlayer != null) { + mediaPlayer.start(); + return; + } + this.videoPath = videoPath; + textureView.setSurfaceTextureListener(surfaceTextureListener); + if (textureView.isAvailable()) { + surface = new Surface(textureView.getSurfaceTexture()); + startPlayback(); + } + } + + private final TextureView.SurfaceTextureListener surfaceTextureListener = + new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture st, int width, int height) { + surface = new Surface(st); + startPlayback(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture st) { + releasePlayer(); + releaseSurface(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture st) { + } + }; + + private void startPlayback() { + if (surface == null || videoPath == null) return; + try { + releasePlayer(); + mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(videoPath); + mediaPlayer.setSurface(surface); + mediaPlayer.setLooping(true); + mediaPlayer.setVolume(0f, 0f); + mediaPlayer.setOnPreparedListener(mp -> { + mp.start(); + //scheduleRestart(); + }); + mediaPlayer.setOnErrorListener((mp, what, extra) -> true); + mediaPlayer.prepareAsync(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /* + // 方案2:周期停止并重新播放,释放 buffer,避免长时间连续合成 + private void restartPlayback() { + startPlayback(); + } + + private void scheduleRestart() { + handler.removeCallbacks(restartRunnable); + handler.postDelayed(restartRunnable, RESTART_INTERVAL_MS); + }*/ + + private void releasePlayer() { + //handler.removeCallbacks(restartRunnable); + final MediaPlayer mp = mediaPlayer; + mediaPlayer = null; + if (mp != null) { + new Thread(() -> { + try { + mp.setOnPreparedListener(null); + mp.setOnErrorListener(null); + if (mp.isPlaying()) { + mp.stop(); + } + mp.release(); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + } + + private void releaseSurface() { + if (surface != null) { + surface.release(); + surface = null; + } + } + + @Override + protected void onStop() { + super.onStop(); + releasePlayer(); + releaseSurface(); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViewPresentation.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViewPresentation.java new file mode 100644 index 00000000..854c931b --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/original/ViewPresentation.java @@ -0,0 +1,182 @@ +package com.dspread.pos.dualScreen.original; + +import android.app.Presentation; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; + + +import com.dspread.pos.dualScreen.manager.PictureData; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义View显示 Presentation(原生实现) + * + *

将外部传入的 View 显示到副屏,可选叠加图片(图片轮播)。

+ */ +public class ViewPresentation extends Presentation { + + private final FrameLayout rootLayout; + /** 轮播图片载体,固定放在自定义 View 的下层作为背景。 */ + private final ImageView picImageView; + private final Handler handler = new Handler(Looper.getMainLooper()); + + // 轮播相关 + private List picDataList; + private int currentIndex = 0; + private long intervalMillis = 3000L; + private boolean isStopSlide = false; + + private final Runnable slideshowRunnable = new Runnable() { + @Override + public void run() { + if (isStopSlide || picDataList == null || picDataList.isEmpty()) return; + showCurrentPic(); + currentIndex = (currentIndex + 1) % picDataList.size(); + handler.postDelayed(this, intervalMillis); + } + }; + + public ViewPresentation(Context context, Display display) { + super(context, display); + rootLayout = new FrameLayout(getContext()); + rootLayout.setBackgroundColor(Color.BLACK); + rootLayout.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // 图片层默认铺满,轮播时按 PictureData 的坐标/尺寸摆放 + picImageView = new ImageView(getContext()); + picImageView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + picImageView.setScaleType(ImageView.ScaleType.FIT_XY); + + setContentView(rootLayout); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setupCutoutMode(); + } + + @Override + public void hide() { + super.hide(); + isStopSlide = true; + } + + /** 让窗口延伸到挖孔(cutout)区域,占满整个副屏 */ + private void setupCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + window.setAttributes(attrs); + } + } + } + + /** + * 显示自定义View + */ + public void showView(View view) { + stopSlideshow(); + rootLayout.removeAllViews(); + if (view == null) return; + // 如果 View 已有 parent,先移除 + if (view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).removeView(view); + } + rootLayout.addView(view); + } + + /** + * 显示自定义View + 图片轮播 + * + *

图片层在底层轮播,自定义 View 叠加在最上层。

+ * + * @param intervalSeconds 轮播间隔(秒),小于 1 时按 1 秒处理 + */ + public void showViewWithPics(View view, List picDataList, int intervalSeconds) { + stopSlideshow(); + rootLayout.removeAllViews(); + picImageView.setVisibility(View.GONE); + rootLayout.addView(picImageView); + if (view != null) { + if (view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).removeView(view); + } + rootLayout.addView(view); + } + this.intervalMillis = Math.max(1, intervalSeconds) * 1000L; + this.picDataList = (picDataList == null) ? null : new ArrayList<>(picDataList); + this.currentIndex = 0; + this.isStopSlide = false; + if (this.picDataList != null && !this.picDataList.isEmpty()) { + handler.post(slideshowRunnable); + } + } + + /** 显示当前索引对应的图片 */ + private void showCurrentPic() { + if (picDataList == null || picDataList.isEmpty()) return; + PictureData pd = picDataList.get(currentIndex); + if (pd == null) return; + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + pd.getWidth() > 0 ? pd.getWidth() : FrameLayout.LayoutParams.WRAP_CONTENT, + pd.getHeight() > 0 ? pd.getHeight() : FrameLayout.LayoutParams.WRAP_CONTENT); + lp.leftMargin = pd.getStartX(); + lp.topMargin = pd.getStartY(); + picImageView.setLayoutParams(lp); + picImageView.setVisibility(View.VISIBLE); + loadImage(pd.getPath(), picImageView); + } + + private void stopSlideshow() { + handler.removeCallbacks(slideshowRunnable); + picDataList = null; + isStopSlide = true; + } + + @Override + protected void onStop() { + super.onStop(); + stopSlideshow(); + } + + private void loadImage(String path, ImageView target) { + if (path == null || path.isEmpty()) return; + File file = new File(path); + if (!file.exists()) return; + try { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.RGB_565; + Bitmap bmp = BitmapFactory.decodeFile(path, opts); + if (bmp != null) { + target.setImageBitmap(bmp); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/AmountDisplayView.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/AmountDisplayView.java new file mode 100644 index 00000000..0282ac94 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/AmountDisplayView.java @@ -0,0 +1,97 @@ +package com.dspread.pos.dualScreen.view; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.dspread.pos_android_app.R; + +import java.util.Locale; + +/** + * 副屏金额显示自定义 View + * + *

白底布局:顶部 "Enter amount" 提示文字、中部大号粗体金额、底部分割线, + * 三者整体垂直居中。金额变化时调用 {@link #setAmount(String)} / {@link #setAmount(long)} 更新文本, + * 再通过 {@link com.dspread.pos.dualScreen.manager.ViceScreenManager#show(View)} 刷新到副屏。

+ */ +public class AmountDisplayView extends FrameLayout { + + private final TextView tvAmount; + + public AmountDisplayView(Context context) { + super(context); + setBackgroundColor(Color.WHITE); + // 作为副屏 Presentation 根容器的子 View,铺满整个副屏 + setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // 内容容器:垂直排列,整体垂直居中,子元素水平居中 + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + contentLayout.setGravity(Gravity.CENTER_HORIZONTAL); + LayoutParams contentLp = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER); + addView(contentLayout, contentLp); + + // 顶部提示文字 + TextView tvHint = new TextView(context); + tvHint.setText(R.string.vice_screen_enter_amount); + tvHint.setTextColor(Color.BLACK); + tvHint.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + tvHint.setGravity(Gravity.CENTER); + contentLayout.addView(tvHint, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 中部金额文字:大号粗体 + tvAmount = new TextView(context); + tvAmount.setText("¥0.00"); + tvAmount.setTextColor(Color.BLACK); + tvAmount.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + tvAmount.setTypeface(Typeface.DEFAULT_BOLD); + tvAmount.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams amountLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + amountLp.topMargin = dp(8); + contentLayout.addView(tvAmount, amountLp); + + // 底部分割线:浅灰细线,贯穿整个宽度 + View divider = new View(context); + divider.setBackgroundColor(Color.parseColor("#E0E0E0")); + LinearLayout.LayoutParams dividerLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, dp(1)); + dividerLp.topMargin = dp(24); + contentLayout.addView(divider, dividerLp); + } + + /** + * 更新副屏显示的金额(原样显示) + */ + public void setAmount(String amountText) { + if (amountText != null) { + tvAmount.setText(amountText); + } + } + + /** + * 更新副屏显示的金额(单位:分),自动格式化为 ¥x.xx + */ + public void setAmount(long amountInCents) { + tvAmount.setText(String.format(Locale.getDefault(), "%s%d.%02d", + getResources().getString(R.string.amount_identification), + amountInCents / 100, amountInCents % 100)); + } + + private int dp(float value) { + return (int) (getResources().getDisplayMetrics().density * value + 0.5f); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentQRPromptView.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentQRPromptView.java new file mode 100644 index 00000000..b17fb7dc --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentQRPromptView.java @@ -0,0 +1,126 @@ +package com.dspread.pos.dualScreen.view; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.dspread.pos_android_app.R; + +/** + * 副屏扫码支付提示自定义 View + * + *

两列布局:左侧红色圆角卡片显示"Trans Amount"+金额, + * 右侧白色描边卡片显示红色圆形扫码图标+提示文字, + * 通过 {@link com.dspread.pos.dualScreen.manager.ViceScreenManager#show(android.view.View)} 显示到副屏。

+ */ +public class PaymentQRPromptView extends LinearLayout { + + private final TextView tvAmount; + + public PaymentQRPromptView(Context context) { + super(context); + setOrientation(HORIZONTAL); + setGravity(Gravity.CENTER); + setBackgroundColor(Color.WHITE); + setPadding(dp(4), dp(4), dp(4), dp(4)); + // 作为副屏 Presentation 根容器的子 View,铺满整个副屏 + setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // 左列:红色圆角卡片,显示 Trans Amount + 金额 + LinearLayout amountCard = new LinearLayout(context); + amountCard.setOrientation(LinearLayout.VERTICAL); + amountCard.setGravity(Gravity.CENTER); + amountCard.setPadding(dp(4), dp(4), dp(4), dp(4)); + amountCard.setBackground(roundedDrawable(Color.parseColor("#FFD32F2F"), dp(8), 0, Color.TRANSPARENT)); + LayoutParams amountCardLp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + amountCardLp.setMargins(0, 0, dp(4), 0); + addView(amountCard, amountCardLp); + + TextView tvTitle = new TextView(context); + tvTitle.setText("Trans Amount"); + tvTitle.setTextColor(Color.WHITE); + tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); + tvTitle.setGravity(Gravity.CENTER); + amountCard.addView(tvTitle); + + tvAmount = new TextView(context); + tvAmount.setTextColor(Color.WHITE); + tvAmount.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + tvAmount.setTypeface(Typeface.DEFAULT_BOLD); + tvAmount.setGravity(Gravity.CENTER); + LayoutParams amountTextLp = new LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + amountTextLp.topMargin = dp(4); + amountCard.addView(tvAmount, amountTextLp); + + // 右列:白色圆角卡片(红色描边),显示扫码图标 + 提示文字 + LinearLayout scanCard = new LinearLayout(context); + scanCard.setOrientation(LinearLayout.VERTICAL); + scanCard.setGravity(Gravity.CENTER); + scanCard.setPadding(dp(4), dp(4), dp(4), dp(4)); + scanCard.setBackground(roundedDrawable(Color.WHITE, dp(8), dp(1), Color.parseColor("#FFD32F2F"))); + LayoutParams scanCardLp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + scanCardLp.setMargins(dp(4), 0, 0, 0); + addView(scanCard, scanCardLp); + + // 红色圆形背景 + 白色扫码图标 + FrameLayout iconWrap = new FrameLayout(context); + GradientDrawable circleBg = new GradientDrawable(); + circleBg.setShape(GradientDrawable.OVAL); + circleBg.setColor(Color.parseColor("#FFD32F2F")); + iconWrap.setBackground(circleBg); + scanCard.addView(iconWrap, new LayoutParams(dp(32), dp(32))); + + ImageView ivScan = new ImageView(context); + ivScan.setImageResource(R.mipmap.ic_salemethod_scan); + ivScan.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + ivScan.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + iconWrap.addView(ivScan, new FrameLayout.LayoutParams( + dp(18), dp(18), Gravity.CENTER)); + + // 提示文字 + TextView tvPrompt = new TextView(context); + tvPrompt.setText("Please place your payment code above the camera."); + tvPrompt.setTextColor(Color.BLACK); + tvPrompt.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); + tvPrompt.setGravity(Gravity.CENTER); + LayoutParams promptLp = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + promptLp.topMargin = dp(4); + scanCard.addView(tvPrompt, promptLp); + } + + /** + * 更新副屏显示的金额 + */ + public void setAmount(String amountText) { + if (amountText != null) { + tvAmount.setText(amountText); + } + } + + private GradientDrawable roundedDrawable(int fillColor, int radius, int strokeWidth, int strokeColor) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setShape(GradientDrawable.RECTANGLE); + drawable.setCornerRadius(radius); + drawable.setColor(fillColor); + if (strokeWidth > 0) { + drawable.setStroke(strokeWidth, strokeColor); + } + return drawable; + } + + private int dp(int value) { + return (int) (getResources().getDisplayMetrics().density * value + 0.5f); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentResultView.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentResultView.java new file mode 100644 index 00000000..2a6b9a4a --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/PaymentResultView.java @@ -0,0 +1,112 @@ +package com.dspread.pos.dualScreen.view; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.dspread.pos_android_app.R; + +/** + * 副屏交易结果自定义 View + * + *

白底布局:左右排列,左侧为交易成功/失败图标,右侧为交易金额。 + * 通过 {@link #setResult(boolean)} 切换图标、{@link #setAmount(String)} 更新金额, + * 再通过 {@link com.dspread.pos.dualScreen.manager.ViceScreenManager#show(View)} 刷新到副屏。

+ */ +public class PaymentResultView extends FrameLayout { + + private final ImageView ivResult; + private final TextView tvAmount; + private final TextView tvErrorMsg; + + public PaymentResultView(Context context) { + super(context); + setBackgroundColor(Color.WHITE); + // 作为副屏 Presentation 根容器的子 View,铺满整个副屏 + setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // 内容容器:垂直排列,整体居中 + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + contentLayout.setGravity(Gravity.CENTER); + addView(contentLayout, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER)); + + // 上半部分:水平排列图标与金额 + LinearLayout rowLayout = new LinearLayout(context); + rowLayout.setOrientation(LinearLayout.VERTICAL); + rowLayout.setGravity(Gravity.CENTER); + contentLayout.addView(rowLayout, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 左侧结果图标 + ivResult = new ImageView(context); + ivResult.setImageResource(R.mipmap.icon_success); + rowLayout.addView(ivResult, new LinearLayout.LayoutParams(dp(40), dp(40))); + + // 右侧交易金额 + tvAmount = new TextView(context); + tvAmount.setTextColor(Color.BLACK); + tvAmount.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); + tvAmount.setTypeface(Typeface.DEFAULT_BOLD); + tvAmount.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams amountLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + //amountLp.leftMargin = dp(16); + rowLayout.addView(tvAmount, amountLp); + + // 下方错误信息 + tvErrorMsg = new TextView(context); + tvErrorMsg.setTextColor(Color.BLACK); + tvErrorMsg.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + tvErrorMsg.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams errorLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + errorLp.topMargin = dp(2); + contentLayout.addView(tvErrorMsg, errorLp); + } + + /** + * 设置交易结果图标 + * + * @param success true 显示成功图标,false 显示失败图标 + */ + public void setResult(boolean success) { + ivResult.setImageResource(success ? R.mipmap.icon_success : R.mipmap.payment_failed); + } + + /** + * 更新副屏显示的交易金额 + */ + public void setAmount(String amountText) { + if (amountText != null) { + tvAmount.setText(amountText); + } + } + + /** + * 更新副屏显示的错误信息 + */ + public void setErrorMsg(String errorMsg) { + if (!TextUtils.isEmpty(errorMsg)) { + tvErrorMsg.setText(errorMsg); + } + } + + private int dp(float value) { + return (int) (getResources().getDisplayMetrics().density * value + 0.5f); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/ProcessingDisplayView.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/ProcessingDisplayView.java new file mode 100644 index 00000000..8004b5b0 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/dualScreen/view/ProcessingDisplayView.java @@ -0,0 +1,96 @@ +package com.dspread.pos.dualScreen.view; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * 副屏"执行中"提示自定义 View + * + *

白底布局:居中显示循环转动的加载动画,下方居中显示处理提示文本。 + * 文本变化时调用 {@link #setMessage(String)} 更新, + * 再通过 {@link com.dspread.pos.dualScreen.manager.ViceScreenManager#show(View)} 刷新到副屏。

+ */ +public class ProcessingDisplayView extends FrameLayout { + + private final TextView tvAmount; + private final TextView tvMessage; + + public ProcessingDisplayView(Context context) { + super(context); + setBackgroundColor(Color.WHITE); + // 作为副屏 Presentation 根容器的子 View,铺满整个副屏 + setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // 内容容器:垂直排列,整体居中 + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + contentLayout.setGravity(Gravity.CENTER); + addView(contentLayout, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + + // 顶部加载动画:无限循环的圆形进度动画 + /* ProgressBar progressBar = new ProgressBar(context); + progressBar.setIndeterminate(true); + LinearLayout.LayoutParams pbLp = new LinearLayout.LayoutParams(dp(24), dp(24)); + pbLp.gravity = Gravity.CENTER_HORIZONTAL; + contentLayout.addView(progressBar, pbLp);*/ + + // 金额文本:位于提示文本上方,大号加粗显示 + tvAmount = new TextView(context); + tvAmount.setTextColor(Color.BLACK); + tvAmount.setTextSize(TypedValue.COMPLEX_UNIT_SP, 26); + tvAmount.setTypeface(Typeface.DEFAULT_BOLD); + tvAmount.setGravity(Gravity.CENTER); + tvAmount.setVisibility(View.GONE); + LinearLayout.LayoutParams amountLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + amountLp.topMargin = dp(12); + contentLayout.addView(tvAmount, amountLp); + + // 底部处理提示文本 + tvMessage = new TextView(context); + tvMessage.setTextColor(Color.BLACK); + tvMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + tvMessage.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams msgLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + msgLp.topMargin = dp(16); + contentLayout.addView(tvMessage, msgLp); + } + + /** + * 更新副屏显示的处理提示文本 + */ + public void setMessage(String msg) { + if (msg != null) { + tvMessage.setText(msg); + } + } + + /** + * 更新副屏显示的金额文本,金额为空时隐藏 + */ + public void setAmount(String amount) { + if (amount == null || amount.isEmpty()) { + tvAmount.setVisibility(View.GONE); + } else { + tvAmount.setText(amount); + tvAmount.setVisibility(View.VISIBLE); + } + } + + private int dp(float value) { + return (int) (getResources().getDisplayMetrics().density * value + 0.5f); + } +} diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeFragment.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeFragment.java index a3ad190c..a60c5309 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeFragment.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeFragment.java @@ -5,22 +5,27 @@ import android.os.CountDownTimer; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; -import com.dspread.pos.common.base.BaseFragmentWithViewCache; -import com.dspread.pos.TitleProviderListener; -import com.dspread.pos.ui.payment.PaymentMethodActivity; -import com.dspread.pos.utils.TRACE; -import com.dspread.pos_android_app.BR; -import com.dspread.pos_android_app.R; -import com.dspread.pos_android_app.databinding.FragmentHomeBinding; - -import me.goldze.mvvmhabit.utils.SPUtils; - - +import com.dspread.pos.common.base.BaseFragmentWithViewCache; +import com.dspread.pos.TitleProviderListener; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; +import com.dspread.pos.dualScreen.view.AmountDisplayView; +import com.dspread.pos.ui.payment.PaymentMethodActivity; +import com.dspread.pos.utils.DeviceModelUtils; +import com.dspread.pos.utils.TRACE; +import com.dspread.pos_android_app.BR; +import com.dspread.pos_android_app.R; +import com.dspread.pos_android_app.databinding.FragmentHomeBinding; + +import me.goldze.mvvmhabit.utils.SPUtils; + + public class HomeFragment extends BaseFragmentWithViewCache implements TitleProviderListener { private boolean canshow = true; private CountDownTimer showTimer; + private ViceScreenManager viceScreenManager; @Override public int initContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -34,7 +39,16 @@ public int initVariableId() { @Override public void initData() { + TRACE.d("HomeFragment initData"); initTimer(); + if(DeviceModelUtils.isD80()){ + viceScreenManager = ViceScreenManager.getInstance(getContext()); + viceScreenManager.power(true); + viceShowDefault(); + } + } + private void viceShowDefault() { + viceScreenManager.showDefaultView(); } private void initTimer() { @@ -64,6 +78,14 @@ public void initViewObservable() { if (!canshow) return; canshow = false; showTimer.start(); + if(DeviceModelUtils.isD80()){ + viceScreenManager = ViceScreenManager.getInstance(getContext()); + if(viceScreenManager.getPowerOnStatus() == 1){ + AmountDisplayView view = new AmountDisplayView(getContext()); + view.setAmount(inputMoney); + viceScreenManager.show(view); + } + } navigateToPaymentMethod(inputMoney); // Obtain the system standard jump animation time diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeViewModel.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeViewModel.java index 543415f9..c143c3ea 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeViewModel.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/home/HomeViewModel.java @@ -7,6 +7,9 @@ import androidx.databinding.ObservableField; import com.dspread.pos.common.base.BaseAppViewModel; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; +import com.dspread.pos.dualScreen.view.AmountDisplayView; +import com.dspread.pos.utils.DeviceModelUtils; import com.dspread.pos.utils.TRACE; import com.dspread.pos_android_app.R; @@ -38,6 +41,14 @@ private void updateAmountDisplay() { if (amountBuilder.length() == 0) { amount.set("$0.00"); amountValid.set(false); + if(DeviceModelUtils.isD80()) { + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(getApplication()); + if(viceScreenManager.getPowerOnStatus() == 1){ + AmountDisplayView view = new AmountDisplayView(getApplication()); + view.setAmount(amount.get()); + viceScreenManager.show(view); + } + } return; } amountValid.set(true); @@ -52,6 +63,16 @@ private void updateAmountDisplay() { String decimalPart = amountStr.substring(amountStr.length() - 2); amount.set(String.format("$%s.%s", intPart, decimalPart)); } + TRACE.i("HomeviewModel amount:" + amountBuilder.toString()); + if(DeviceModelUtils.isD80()) { + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(getApplication()); + if(viceScreenManager.getPowerOnStatus() == 1){ + AmountDisplayView view = new AmountDisplayView(getApplication()); + view.setAmount(amount.get()); + viceScreenManager.show(view); + } + } + } public void clearAmount() { diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/main/MainActivity.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/main/MainActivity.java index fc183b1c..5df8286f 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/main/MainActivity.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/main/MainActivity.java @@ -1,6 +1,7 @@ package com.dspread.pos.ui.main; import android.content.pm.ActivityInfo; +import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; @@ -8,10 +9,12 @@ import android.widget.ImageView; import com.dspread.pos.common.manager.FragmentCacheManager; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; import com.dspread.pos.posAPI.POSManager; import com.dspread.pos.ui.home.HomeFragment; import com.dspread.pos.ui.transaction.SerachKeyboardUtils; import com.dspread.pos.upgrade.CustomUpgradeCallback; +import com.dspread.pos.utils.DeviceModelUtils; import com.dspread.pos.utils.Mydialog; import com.dspread.pos.utils.TRACE; import com.dspread.pos_android_app.BR; @@ -78,11 +81,33 @@ protected void onResume() { super.onResume(); } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + // 切回 HomeFragment + if (viewModel != null) { + TRACE.d("App started, 1"); + viewModel.handleNavigationItemClick(R.id.nav_home); + } else if (viewPager != null) { + TRACE.d("App started, 2"); + viewPager.setCurrentItem(MainFragmentAdapter.FRAGMENT_HOME, false); + updateNavigationMenuSelection(MainFragmentAdapter.FRAGMENT_HOME); + } + // 重新执行 HomeFragment.initData 中的副屏初始化(D80) + if (DeviceModelUtils.isD80()) { + TRACE.d("App started, 3"); + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(getApplication()); + viceScreenManager.showDefaultView(); + } + } + @Override public void initData() { super.initData(); // Reset update dialog shown flag on app start SPUtils.getInstance().put("update_dialog_shown", false); + TRACE.d("App started, update dialog shown flag reset"); drawerLayout = binding.drawerLayout; navigationView = binding.navView; @@ -223,9 +248,12 @@ protected void onDestroy() { if (viewPager != null) { viewPager = null; } - // Shutdown background executor backgroundExecutor.shutdown(); + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(getApplication()); + if(viceScreenManager.getPowerOnStatus() == 1){ + viceScreenManager.power(false); + } } @Override diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java index b9049e8a..4c4d7fd5 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentActivity.java @@ -2,18 +2,23 @@ import android.app.Dialog; import android.content.Intent; +import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Html; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; +import android.widget.FrameLayout; import android.widget.ListView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.databinding.DataBindingUtil; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; +import com.dspread.pos.dualScreen.view.ProcessingDisplayView; import com.dspread.pos.posAPI.ConnectionServiceCallback; import com.dspread.pos.posAPI.POSManager; import com.dspread.pos.posAPI.PaymentServiceCallback; @@ -69,6 +74,7 @@ public class PaymentActivity extends BaseActivity { + currentMethodIndex = 0; + paymentMethodsLayout.setSelectedPaymentMethod(currentMethodIndex); + handlePaymentMethodSelection(0); + })); + + // Scan Code 点击事件 + methodRow.addView(createMethodItem(R.mipmap.ic_salemethod_scan, "Scan Code", v -> { + currentMethodIndex = 1; + paymentMethodsLayout.setSelectedPaymentMethod(currentMethodIndex); + handlePaymentMethodSelection(1); + })); + + // show + viceScreenManager.show(viceRoot); + } + } + } + + + /** + * 创建副屏支付方式选项(图标 + 文字,带点击事件) + */ + private LinearLayout createMethodItem(int iconRes, String label, View.OnClickListener listener) { + LinearLayout item = new LinearLayout(this); + item.setOrientation(LinearLayout.VERTICAL); + item.setGravity(Gravity.CENTER); + item.setClickable(true); + item.setFocusable(true); + item.setPadding(dp(6), dp(6), dp(6), dp(6)); + item.setBackground(createMethodItemBackground()); + // 宽度采用 0dp + weight=1 均分副屏可用宽度,避免固定宽度超屏导致下方文字被裁切 + LinearLayout.LayoutParams itemLp = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); + itemLp.setMargins(dp(6), 0, dp(6), 0); + item.setLayoutParams(itemLp); + + ImageView icon = new ImageView(this); + icon.setImageResource(iconRes); + icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + item.addView(icon, new LinearLayout.LayoutParams(dp(32), dp(32))); + + TextView labelTv = new TextView(this); + labelTv.setText(label); + labelTv.setTextColor(Color.parseColor("#ff030303")); + labelTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + labelTv.setGravity(Gravity.CENTER); + labelTv.setSingleLine(true); + LinearLayout.LayoutParams labelLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + labelLp.topMargin = dp(4); + item.addView(labelTv, labelLp); + + item.setOnClickListener(listener); + return item; + } + + /** + * 创建副屏支付方式选项的圆角边框背景(按压态变色) + */ + private StateListDrawable createMethodItemBackground() { + GradientDrawable normalDrawable = new GradientDrawable(); + normalDrawable.setShape(GradientDrawable.RECTANGLE); + normalDrawable.setCornerRadius(dp(2)); + normalDrawable.setStroke(dp(1), Color.parseColor("#BCBCBC")); + normalDrawable.setColor(Color.WHITE); + + GradientDrawable pressedDrawable = new GradientDrawable(); + pressedDrawable.setShape(GradientDrawable.RECTANGLE); + pressedDrawable.setCornerRadius(dp(2)); + pressedDrawable.setStroke(dp(1), Color.parseColor("#ffe47579")); + pressedDrawable.setColor(Color.parseColor("#ffffe9e9")); + + StateListDrawable states = new StateListDrawable(); + states.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable); + states.addState(new int[]{}, normalDrawable); + return states; + } + + private int dp(int value) { + return (int) (getResources().getDisplayMetrics().density * value + 0.5f); + } private void handlePaymentMethodSelection(int methodIndex) { // Prevent duplicate clicks flag @@ -129,6 +278,17 @@ private void startScanCodePayment() { intent.putExtra("amount", amount); startActivity(intent);*/ TRACE.d("PayMethodActivity startScanCodePayment"); + + if(DeviceModelUtils.isD80()){ + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(this); + if(viceScreenManager.getPowerOnStatus() == 1){ + // 副屏显示扫码支付提示:左侧金额卡片,右侧扫码图标与提示文字 + PaymentQRPromptView promptView = new PaymentQRPromptView(this); + promptView.setAmount(viewModel.totalAmount.get()); + viceScreenManager.show(promptView); + } + } + initScanCode(); } diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusActivity.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusActivity.java index e0f8c5a1..399b89d9 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusActivity.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusActivity.java @@ -6,12 +6,14 @@ import android.os.Bundle; import android.text.TextUtils; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; +import com.dspread.pos.dualScreen.view.PaymentResultView; +import com.dspread.pos.utils.DeviceModelUtils; import com.dspread.pos.utils.DeviceUtils; import com.dspread.pos_android_app.BR; import com.dspread.pos_android_app.R; import com.dspread.pos_android_app.databinding.ActivityPaymentstatusBinding; - import java.util.HashMap; import java.util.Map; @@ -45,18 +47,30 @@ public void initData() { maskedPAN = getStringExtraSafely(intent, KEY_MASKED_PAN); terminalTime = getStringExtraSafely(intent, KEY_TERMINAL_TIME); errorMsg = getStringExtraSafely(intent, KEY_ERROR_MSG); - - viewModel.isD70DisplayScreen.set(MODEL_D70.equals(Build.MODEL)); - if (isValidAmount(amount)) { handleTransactionSuccess(); } else { handleTransactionFailure(); } - + if(DeviceModelUtils.isD80()){ + ViceScreenManager viceScreenManager = ViceScreenManager.getInstance(PaymentStatusActivity.this); + if(viceScreenManager.getPowerOnStatus() == 1){ + // 显示副屏:左侧结果图标 + 右侧交易金额 + 下面是交易成功或交易失败 + PaymentResultView view = new PaymentResultView(PaymentStatusActivity.this); + boolean isSuccess = isValidAmount(amount); + view.setResult(isSuccess); + view.setErrorMsg("payment success"); + if (isSuccess) { + view.setAmount("$ " + DeviceUtils.convertAmountToCents(amount)); + } else { + view.setErrorMsg(errorMsg); + } + viceScreenManager.show(view); + } + } viewModel.isShouwPrinting.set(DeviceUtils.isPrinterDevices()); } @@ -82,14 +96,11 @@ private boolean isValidAmount(String amount) { */ private void handleTransactionSuccess() { String amountInCents = DeviceUtils.convertAmountToCents(amount); - viewModel.displayAmount(amountInCents); viewModel.setTransactionSuccess(); - Map receiptData = createReceiptData(amountInCents, maskedPAN, terminalTime); viewModel.sendTranReceipt(receiptData); } - /** * 创建收据数据 */ diff --git a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusViewModel.java b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusViewModel.java index 1fdb7b54..665b867e 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusViewModel.java +++ b/pos_android_studio_demo/pos_android_app/src/main/java/com/dspread/pos/ui/payment/PaymentStatusViewModel.java @@ -4,7 +4,9 @@ import android.content.Intent; import com.dspread.pos.common.base.BaseAppViewModel; +import com.dspread.pos.dualScreen.manager.ViceScreenManager; import com.dspread.pos.ui.printer.activities.PrintTicketActivity; +import com.dspread.pos.utils.DeviceModelUtils; import com.dspread.pos.utils.TRACE; import java.util.Map; @@ -68,6 +70,9 @@ public void sendTranReceipt(Map map) { isNavigating = true; try { finish(); + if (DeviceModelUtils.isD80()) { + ViceScreenManager.getInstance(context).showDefaultView(); + } } catch (Exception e) { isNavigating = false; } diff --git a/pos_android_studio_demo/pos_android_app/src/main/res/drawable/second_bg.png b/pos_android_studio_demo/pos_android_app/src/main/res/drawable/second_bg.png new file mode 100644 index 00000000..d4ba34fa Binary files /dev/null and b/pos_android_studio_demo/pos_android_app/src/main/res/drawable/second_bg.png differ diff --git a/pos_android_studio_demo/pos_android_app/src/main/res/layout/vice_default.xml b/pos_android_studio_demo/pos_android_app/src/main/res/layout/vice_default.xml new file mode 100644 index 00000000..00a15933 --- /dev/null +++ b/pos_android_studio_demo/pos_android_app/src/main/res/layout/vice_default.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/pos_android_studio_demo/pos_android_app/src/main/res/values/strings.xml b/pos_android_studio_demo/pos_android_app/src/main/res/values/strings.xml index be0d33d7..0ed9618a 100644 --- a/pos_android_studio_demo/pos_android_app/src/main/res/values/strings.xml +++ b/pos_android_studio_demo/pos_android_app/src/main/res/values/strings.xml @@ -194,6 +194,7 @@ Card error Declined Please enter amount + Enter amount Incorrect PIN Please insert card Not accepted @@ -459,7 +460,22 @@ content MaxHeight - 欢迎光临(Simplified Chinese)\n歡迎光臨(traditional chinese)\nWelcome(English)\n어서 오세요.(Korean)\nいらっしゃいませ(Japanese)\nWillkommen in der(Germany)\nSouhaits de bienvenue(France)\nยินดีต้อนรับสู่(Thai)\nДобро пожаловать(Russian)\nBenvenuti a(Italian)\nvítejte v(Czech)\nBEM - vindo Ao(Portutuese)\nمرحبا بكم في(Arabic)\nBienvenido,transacción(Spanish)\nخوش اومدی(Farsi)\n + 欢迎光临(Simplified Chinese) +歡迎光臨(traditional chinese) +Welcome(English) +어서 오세요.(Korean) +いらっしゃいませ(Japanese) +Willkommen in der(Germany) +Souhaits de bienvenue(France) +ยินดีต้อนรับสู่(Thai) +Добро пожаловать(Russian) +Benvenuti a(Italian) +vítejte v(Czech) +BEM - vindo Ao(Portutuese) +مرحبا بكم في(Arabic) +Bienvenido,transacción(Spanish) +خوش اومدی(Farsi) + LEFT RIGHT CENTER @@ -556,4 +572,38 @@ PLS SEE PHONE, and pls wait for cardholder to confirm, then to try again Transaction failed because of the network! + + + + + Connected + Disconnected + No secondary display detected, please connect a secondary display device + + %1$s succeeded + %1$s failed: secondary display not connected + %1$s failed: invalid parameter + %1$s failed: file not found + %1$s failed: hardware not supported + %1$s failed: display error + %1$s failed: code=%2$d + + Secondary display not connected + Secondary #%1$d · %2$d × %3$d + Refresh rate: %1$.0f Hz + Landscape + Portrait + Type: %1$s + Touch: %1$s + Supported + Not reported + Supported modes: %1$d · current: %2$dx%3$d@%4$.0fHz + Name: %1$s + Secure + Presentation + Round + Touch + Not focusable + Standard display + \ No newline at end of file