Indicate whether to send a cookie in a cross-site request by specifying its SameSite attribute

最近在浏览器(`Chrome 85.0.4183.121`)调试网页的时候,出现如下警告信息:

Indicate whether to send a cookie in a cross-site request by specifying its SameSite attribute
Because a cookie's SameSite attribute was not set or is invalid, it defaults to SameSite=Lax, which prevents the cookie from being sent in a cross-site request. This behavior protects user data from accidentally leaking to third parties and cross-site request forgery.

Resolve this issue by updating the attributes of the cookie:
Specify SameSite=None and Secure if the cookie should be sent in cross-site requests. This enables third-party use.
Specify SameSite=Strict or SameSite=Lax if the cookie should not be sent in cross-site requests

22 cookies
3 requests
Learn more: SameSite cookies explained

继续阅读Indicate whether to send a cookie in a cross-site request by specifying its SameSite attribute

WordPress隐藏前台登录用户名

最近在查看`Limit Login Attempts`日志的时候,发现大量的登陆尝试,关键是登陆使用的用户名是正确的。感到很奇怪,用户名是如何泄漏的呢?

网上搜寻了一下,发现原来用户名是被`WordPress`主动泄漏出来的。

`WordPress`鼠标点击前台用户名,浏览器就会跳转到 https://www.mobibrw.com/author/username 这个链接,这样一来就直接暴露了登陆用户名,哪怕你已经在前台使用了昵称。这是个很大的安全隐患!

后台登陆用户名也可以这样查看到:https://www.mobibrw.com/?author=1,多用户的可以把 1 变为 2、3、4、5 等,就可以在地址栏查看到各个用户名。

为了避免暴露登陆用户名,我们需要配置禁止用户名显示。我们需要在当前使用的主题的`functions.php`中增加如下函数

<?php
/////////////////////隐藏用户名信息相关/////////////////////

  // 隐藏用户名
  add_filter('author_link', function($link, $author_id, $author_nicename){
    $author  = get_userdata($author_id);
  
    if(sanitize_title($author->user_login) == $author_nicename){
      global $wp_rewrite;
      $link  = $wp_rewrite->get_author_permastruct();
      $link  = str_replace('%author%', $author_id, $link);
      $link  = home_url(user_trailingslashit($link));
    }
    return $link;
  }, 10, 3);

  // 原作者页直接404
  add_action('pre_get_posts',  function($wp_query) {
    if($wp_query->is_main_query() && $wp_query->is_author()) {
      if($author_name = $wp_query->get('author_name')){
        $author_name = sanitize_title_for_query($author_name);
        $author = get_user_by('slug', $author_name);
        if($author) {
          if(sanitize_title($author->user_login) == $author->user_nicename){
            $wp_query->set_404();
          }
        } else {
          if(is_numeric($author_name)) {
            $wp_query->set('author_name', '');
            $wp_query->set('author', $author_name);
          }
        }
      }
    }
  });

  //修改body_class
  add_filter('body_class', function($classes) {
    if(is_author()) {
      global $wp_query;
      $author = $wp_query->get_queried_object();
      if(sanitize_title($author->user_login) == $author->user_nicename) {
        $author_class = 'author-'.sanitize_html_class($author->user_nicename, $author->ID);
        $classes = array_diff($classes, [$author_class]);
      }
    }
    return $classes;
  });

  //修改comment_class
  add_filter('comment_class', function ($classes) {
    foreach($classes as $key => $class) {
      if(strstr($class, 'comment-author-')) {
        unset($classes[$key]);
      }
    }
    return $classes;
  });

  //禁用 REST API 过滤部分端点
  //解决通过 https://www.mobibrw.com/wp-json/wp/v2/users 获取用户列表的问题
  add_filter( 'rest_endpoints', function( $endpoints ){
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
  });

// -- END ---------------------------------------- 
?>

参考链接


com.android.tools.r8.ApiLevelException: Invoke-customs are only supported starting with Android O

com.android.builder.dexing.DexArchiveBuilderException: com.android.builder.dexing.DexArchiveBuilderException: Failed to process /Users/Project/GitLab/rsdk-client-plugin-android/Android/Plugins/AndroidStudio/sq37gameshskd/riversdk_core_libs/libs/okhttp-3.14.2.jar
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:593)
	at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677)
	at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:720)
	at com.android.ide.common.internal.WaitableExecutor.waitForTasksWithQuickFail(WaitableExecutor.java:146)
	at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.transform(DexArchiveBuilderTransform.java:308)
	at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:221)
	at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:217)
	at com.android.builder.profile.ThreadRecorder.record(ThreadRecorder.java:102)
	at com.android.build.gradle.internal.pipeline.TransformTask.transform(TransformTask.java:212)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
	at org.gradle.api.internal.project.taskfactory.IncrementalTaskAction.doExecute(IncrementalTaskAction.java:46)
	at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
	at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:121)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:110)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:92)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:70)
	at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
	at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:62)
	at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
	at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:60)
	at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:97)
	at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:87)
	at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
	at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
	at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:626)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:581)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:59)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:128)
	at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
	at org.gradle.execution.DefaultBuildExecuter.access$000(DefaultBuildExecuter.java:23)
	at org.gradle.execution.DefaultBuildExecuter$1.proceed(DefaultBuildExecuter.java:43)
	at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:46)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:30)
	at org.gradle.initialization.DefaultGradleLauncher$ExecuteTasks.run(DefaultGradleLauncher.java:314)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.initialization.DefaultGradleLauncher.runTasks(DefaultGradleLauncher.java:204)
	at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:134)
	at org.gradle.initialization.DefaultGradleLauncher.executeTasks(DefaultGradleLauncher.java:109)
	at org.gradle.internal.invocation.GradleBuildController$1.call(GradleBuildController.java:78)
	at org.gradle.internal.invocation.GradleBuildController$1.call(GradleBuildController.java:75)
	at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:152)
	at org.gradle.internal.invocation.GradleBuildController.doBuild(GradleBuildController.java:100)
	at org.gradle.internal.invocation.GradleBuildController.run(GradleBuildController.java:75)
	at org.gradle.tooling.internal.provider.runner.BuildModelActionRunner.run(BuildModelActionRunner.java:53)
	at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
	at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
	at org.gradle.tooling.internal.provider.ValidatingBuildActionRunner.run(ValidatingBuildActionRunner.java:32)
	at org.gradle.launcher.exec.RunAsBuildOperationBuildActionRunner$1.run(RunAsBuildOperationBuildActionRunner.java:43)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.launcher.exec.RunAsBuildOperationBuildActionRunner.run(RunAsBuildOperationBuildActionRunner.java:40)
	at org.gradle.tooling.internal.provider.SubscribableBuildActionRunner.run(SubscribableBuildActionRunner.java:51)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:47)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:30)
	at org.gradle.launcher.exec.BuildTreeScopeBuildActionExecuter.execute(BuildTreeScopeBuildActionExecuter.java:39)
	at org.gradle.launcher.exec.BuildTreeScopeBuildActionExecuter.execute(BuildTreeScopeBuildActionExecuter.java:25)
	at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:80)
	at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:53)
	at org.gradle.tooling.internal.provider.ServicesSetupBuildActionExecuter.execute(ServicesSetupBuildActionExecuter.java:57)
	at org.gradle.tooling.internal.provider.ServicesSetupBuildActionExecuter.execute(ServicesSetupBuildActionExecuter.java:32)
	at org.gradle.tooling.internal.provider.GradleThreadBuildActionExecuter.execute(GradleThreadBuildActionExecuter.java:36)
	at org.gradle.tooling.internal.provider.GradleThreadBuildActionExecuter.execute(GradleThreadBuildActionExecuter.java:25)
	at org.gradle.tooling.internal.provider.ParallelismConfigurationBuildActionExecuter.execute(ParallelismConfigurationBuildActionExecuter.java:43)
	at org.gradle.tooling.internal.provider.ParallelismConfigurationBuildActionExecuter.execute(ParallelismConfigurationBuildActionExecuter.java:29)
	at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:69)
	at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:30)
	at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:59)
	at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:44)
	at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:45)
	at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:30)
	at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:67)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:37)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)
	at org.gradle.util.Swapper.swap(Swapper.java:38)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:55)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:62)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:82)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
	at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)
	at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:295)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)
Caused by: com.android.builder.dexing.DexArchiveBuilderException: Failed to process /Users/Project/GitLab/rsdk-client-plugin-android/Android/Plugins/AndroidStudio/sq37gameshskd/riversdk_core_libs/libs/okhttp-3.14.2.jar
	at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.launchProcessing(DexArchiveBuilderTransform.java:805)
	at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.lambda$convertToDexArchive$8(DexArchiveBuilderTransform.java:730)
	at java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1424)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: com.android.builder.dexing.DexArchiveBuilderException: Error while dexing.
The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}
See https://developer.android.com/studio/write/java8-support.html for details. Alternatively, increase the minSdkVersion to 26 or above.

	at com.android.builder.dexing.D8DexArchiveBuilder.getExceptionToRethrow(D8DexArchiveBuilder.java:175)
	at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:152)
	at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.launchProcessing(DexArchiveBuilderTransform.java:800)
	... 6 more
Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete
	at com.android.tools.r8.utils.ExceptionUtils.withCompilationHandler(ExceptionUtils.java:76)
	at com.android.tools.r8.utils.ExceptionUtils.withD8CompilationHandler(ExceptionUtils.java:45)
	at com.android.tools.r8.D8.run(D8.java:88)
	at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:147)
	... 7 more
Caused by: com.android.tools.r8.utils.AbortException
	at com.android.tools.r8.utils.Reporter.failIfPendingErrors(Reporter.java:77)
	at com.android.tools.r8.utils.Reporter.fatalError(Reporter.java:67)
	at com.android.tools.r8.utils.ExceptionUtils.withCompilationHandler(ExceptionUtils.java:62)
	... 10 more
	Suppressed: com.android.tools.r8.ApiLevelException: Invoke-customs are only supported starting with Android O (--min-api 26)
		at com.android.tools.r8.dex.FileWriter.checkThatInvokeCustomIsAllowed(FileWriter.java:1296)
		at com.android.tools.r8.dex.FileWriter.writeCallSite(FileWriter.java:635)
		at com.android.tools.r8.dex.FileWriter.writeFixedSectionItems(FileWriter.java:302)
		at com.android.tools.r8.dex.FileWriter.generate(FileWriter.java:193)
		at com.android.tools.r8.dex.ApplicationWriter.writeDexFile(ApplicationWriter.java:356)
		at com.android.tools.r8.dex.ApplicationWriter.lambda$write$2(ApplicationWriter.java:188)
		at java.util.concurrent.FutureTask.run(FutureTask.java:266)
		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
		at java.lang.Thread.run(Thread.java:748)
		Suppressed: java.util.concurrent.ExecutionException: com.android.tools.r8.ApiLevelException: Invoke-customs are only supported starting with Android O (--min-api 26)
			at java.util.concurrent.FutureTask.report(FutureTask.java:122)
			at java.util.concurrent.FutureTask.get(FutureTask.java:192)
			at com.android.tools.r8.utils.ThreadUtils.awaitFutures(ThreadUtils.java:21)
			at com.android.tools.r8.dex.ApplicationWriter.write(ApplicationWriter.java:216)
			at com.android.tools.r8.D8.run(D8.java:167)
			at com.android.tools.r8.D8.lambda$run$1(D8.java:91)
			at com.android.tools.r8.utils.ExceptionUtils.withCompilationHandler(ExceptionUtils.java:58)
			at com.android.tools.r8.utils.ExceptionUtils.withD8CompilationHandler(ExceptionUtils.java:45)
			at com.android.tools.r8.D8.run(D8.java:88)
			at com.android.builder.dexing.D8DexArchiveBuilder.convert(D8DexArchiveBuilder.java:147)
			at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.launchProcessing(DexArchiveBuilderTransform.java:800)
			at com.android.build.gradle.internal.transforms.DexArchiveBuilderTransform.lambda$convertToDexArchive$8(DexArchiveBuilderTransform.java:730)
			at java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1424)
			at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
			at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
			at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
			at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
	[CIRCULAR REFERENCE:com.android.tools.r8.ApiLevelException: Invoke-customs are only supported starting with Android O (--min-api 26)]

:app:transformClassesWithDexBuilderForRelease FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:transformClassesWithDexBuilderForRelease'.

原因是某些第三方库使用了`Java 1.8`的`API`,导致整个项目必须使用`Java 1.8`。

解决方法就是增加如下编译命令

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

完整的例子如下:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'pub.devrel:easypermissions:2.0.1'
    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

参考链接


Hudson/Jenkins -- 消除svn警告: "clock of the subversion server appears to be out of sync"

`Hudson/Jenkins`中配置`svn`的`URL`时,有时会出现如下警告信息:

“WARNING: clock of the subversion server appears to be out of sync. This can result in inconsistent check out behavior”

基本选择每次重新`checkout`最新的版本,也会出现该警告信息。

并且经常无法获取到最新的代码,要等几分钟之后才能拉取到最新的代码。

这个在编译的时候有时候会有这个警告,主要是`svn`等服务器的时钟和本机的时钟有偏差,比如`svn`的时钟比较快,`jenkins`的时钟慢,当你提交`svn`的时候,紧接着进行`jenkins`构建,这个时候`jenkins`的`svn update`由于时钟原因就更新不到刚才的数据,但过了几分钟之后再进行更新的时候,超过两台机器的时间差,在次进行`jenkins`构建,就可以`update`到数据。

继续阅读Hudson/Jenkins -- 消除svn警告: "clock of the subversion server appears to be out of sync"

Android 6.0/10.0扫描不到Ble设备需开启位置权限

蓝牙设备扫描需要定位权限的原因在于蓝牙定位的存在,蓝牙定位属于精确定位,因此需要开启精确定位权限,而精确定位跟`GPS`又是直接相关的,因此才会导致`GPS`权限需要获取。

项目中需要用到Android Ble蓝牙4.0开发技术,于是开启了蓝牙填坑之旅,说实话,蓝牙开发坑真多,跳出一个又进入下一个,每次遇到 问题,就觉得不可能解决了,还好在自己的摸索中,都一一的化解了,以此来记录安卓蓝牙开发的心得。

     接手的蓝牙开发项目,原来的同事已经写好,不用再去写,开始也就大概看了看Android蓝牙开发相关资料,对比项目中的蓝牙开发代码,发现发现搜索、连接蓝牙有一样的处理也有不一样的处理,想着项目不一样,代码处理肯定不一样,于是就没去深究,毕竟项目运行起来使用正常,然而,没过几天,客户提出蓝牙搜索太慢,然后就去调试,先看代码是搜索蓝牙8秒钟才显示搜索到的蓝牙设备列表,查看日志,其实不到8秒就搜索到外围设备,只是搜索8秒后停止扫描才显示蓝牙设备,那就把显示成3、4秒呗。
        正这样想,同事拿来一部荣耀8,装的APP怎么就是搜不到蓝牙设备,然后调试其他手机,都能搜索到,于是就纳闷了,荣耀8怎么就是搜索不到蓝牙设备呢,其他手机都能,再看一遍代码,也没有专门针对荣耀8处理的地方,再想,荣耀8是系统7.0的手机,是不是跟权限没开有关呢,对比其他华为手机,申请的蓝牙权限也都开着,荣耀8就是搜索不到,是不是荣耀8手机有问题,同事告知下载一个第三方蓝牙调试工具查查看,令人疑惑的是荣耀8用第三方蓝牙调试工具立刻就能扫描显示出来,试了几次都是立刻扫描出来,当时心想,我靠,什么鬼,这么迅速就能把蓝牙设备扫描显示出来,领导也说,别人的调试工具都能做这么快,你也得做这么快,想着目前的代码都是几秒后才能搜索到,目前的荣耀8还搜索不出来,客户那边还一个劲儿的给老板吐槽太慢,于是压力剧增,真正开始了蓝牙开发不归之旅。
     但是再考虑索慢时,领导说,得把荣耀8搜索不到的问题先解决,因为荣耀8是项目实施人员给客户安装硬件调试数据要用的,那就先把搜索慢暂时放一放,想着一边搜索慢没还没着落,荣耀8手机就是搜索不到,想想就头大,查找荣耀8为什么搜索不到蓝牙设备的资料,也没发现有效的办法,这时,用荣耀手机的人说,另一个公司的项目app(简称B吧,荣耀8搜索不到蓝牙设备的项目简称A吧)就能正常搜索到蓝牙设备,一打开调试还真是每次都能搜索到,看了一下两个项目的代码:

private BluetoothAdapter btAdapt;
    private List<BluetoothDevice> listDevices = new ArrayList<BluetoothDevice>();
    private List<TieBean> listDeviceName = new ArrayList<TieBean>();
    private boolean bIsConnected;
    private BluetoothDevice myBluetooth;
    private public BluetoothGatt mBluetoothGatt;
    private TextView tv_seacher_ble;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);//去掉标题栏
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//锁屏
        tv_seacher_ble = (LinearLayout) findViewById(R.id.tv_seacher_ble);//linear Layout
 
        BluetoothAdapter btAdapt = BluetoothAdapter.getDefaultAdapter();// 初始化本机蓝牙功能 
      
        IntentFilter intent = new IntentFilter();
        intent.addAction(BluetoothDevice.ACTION_FOUND);// 用BroadcastReceiver来取得搜索结果
        intent.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        intent.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
        intent.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
        mActivity.registerReceiver(searchDevicesBroadcast, intent);
       
 tv_seacher_ble.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                btAdapt.startDiscovery();
            }
        });
    }
 
 
    //搜索到的蓝牙设备在这里
    private BroadcastReceiver searchDevicesBroadcast = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if (TextUtils.isEmpty(device.getName())) {
                return;
            }
            ToastUtil.showToast(mActivity, "蓝牙设备 " + device.getName());
            String str = device.getName() + "|" + device.getAddress();
            if (listDeviceName.indexOf(str) == -1) {// 防止重复添加
                listDevices.add(device); // 获取设备名称和mac地址
                listDeviceName.add(new TieBean(str));
            }
        }
    };
 
 
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //断开设备
            if (msg.what == 1) {
                mBluetoothGatt.disconnect();
                //连接设备
            } else if (msg.what == 2) {
                ToastUtil.showToast(mActivity, "开始搜索蓝牙设备");
                if (btAdapt.getState() == BluetoothAdapter.STATE_OFF) {// 如果蓝牙还没开启
                    ToastUtil.showToast(mActivity, "请先打开蓝牙");
                    return;
                }
                if (btAdapt.isDiscovering())
                    btAdapt.cancelDiscovery();
                listDevices.clear();
                listDeviceName.clear();
 
                btAdapt.startDiscovery();
                //定时器操作
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //ToastUtil.showToast(mActivity,"搜索蓝牙完毕");
                        btAdapt.cancelDiscovery();
                        closeProgressDialog();
                        //其中BuildBean,TieAdapter是在app的build.gradle中dependencies {compile 'com.dou361.dialogui:jjdxm-dialogui:1.0.3'};
                        TieAdapter adapter = new TieAdapter(mContext, listDeviceName, true);
                        BuildBean buildBean = DialogUIUtils.showMdBottomSheet(mActivity, true, "", listDeviceName, 0, new DialogUIItemListener() {
                            @Override
                            public void onItemClick(CharSequence text, int position) {
                                myBluetooth = listDevices.get(position);
                                //根据蓝牙地址获取远程蓝牙设备
                                final BluetoothDevice device = btAdapt.getRemoteDevice(myBluetooth.getAddress());
                                if (device == null) {
                                    return;
                                }
                                mBluetoothGatt = device.connectGatt(mActivity, false, mGattCallback);
 
                            }
                        });
                        buildBean.mAdapter = adapter;
                        buildBean.show();
                    }
                }, 8000);    //延时8s执行
            } else if (msg.what == 3) {
                tv_seacher_ble.setText("断开蓝牙");
            } else if (msg.what == 4) {
                tv_seacher_ble.setText("连接蓝牙");
                mBluetoothGatt.close();
            }
            super.handleMessage(msg);
        }
    };

      两个项目中的蓝牙适配器的获取,去发现,动态注册广播、广播接收蓝牙搜索结果、蓝牙连接都一样,至于手机是否支持 ble 4.0,蓝牙是否开启的逻辑,连接上对数据的处理、断开mBluetoothGatt.disconnect(),关闭mBluetoothGatt.close() 的逻辑网上有很多,我这里就不在详说;
       经过2个项目对比,发现就蓝牙搜索多久停止不一样,荣耀8能搜索到蓝牙设备的时间项目代码设置是10秒,而搜索不到的项目是设置8秒,带着半信半疑把项目A的蓝牙搜索时间由8秒改成10秒,运行,奇迹还真是出现了,荣耀8可以正常搜索到蓝牙设备了,反复测了好多次都能正常搜索到蓝牙设备,我是不信邪的人,还改成8秒试试,果然还是搜索不到。
     于是和领导说,你看荣耀8必须10秒才能搜索 到蓝牙设备,搜索到的蓝牙设备列表不晚点展示不行啊,领导自然 是不同意,拿出第三方蓝牙提哦啊是工具说,那第三方就能很快搜索到,你想想其他法吧,想着有的手机快,有的手机慢,提出意见,能不能搜索到一个设备就展示到列表,不必等10秒后再展示,这样体验好些,反正领导说不行,既然领导说不行,那就回去研究吧。
    查了很多蓝牙搜索慢怎么办?看了半天,当然这期间调试过各种其他方法,都没用,功夫不负有心人,其中一个博客说:btAdapt.startDiscovery();搜索返回的结果慢, 现在都用这个 btAdapt.startLeScan(getmLeScanCallback); 其他也没多说什么,喜出望外,这不是正是我要的吗,于是改蓝牙搜索,上代码:

package com.hand.hand16.activity;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;

import com.dou361.dialogui.DialogUIUtils;
import com.dou361.dialogui.adapter.TieAdapter;
import com.dou361.dialogui.bean.BuildBean;
import com.dou361.dialogui.bean.TieBean;
import com.dou361.dialogui.listener.DialogUIItemListener;

import java.util.ArrayList;
import java.util.List;


public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public boolean bIsConnected;
    public BluetoothGatt mBluetoothGatt;//中央使用和处理数据
    public BluetoothGattCharacteristic mNotifyCharacteristic;
    List<BluetoothDevice> listDevices = new ArrayList<BluetoothDevice>();
    List<TieBean> listDeviceName = new ArrayList<TieBean>();
    private TextView tv_seacher_ble;
    private TieAdapter tieAdapter;
    private Vibrator vibrator;
    private BluetoothDevice mBluetoothDevice;
    private BluetoothAdapter btAdapt;
    private BluetoothDevice myBluetooth;
    private boolean mScanning;
    //蓝牙服务
    private String strDevice = "";
    BluetoothAdapter.LeScanCallback getmLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    boolean isSame = true;
                    if (bluetoothDevice != null && bluetoothDevice.getName() != null) {
                        LogUtil.e("蓝牙设备==" + bluetoothDevice.getName() + "|" + bluetoothDevice.getAddress());
                        strDevice = bluetoothDevice.getName() + "|" + bluetoothDevice.getAddress();
                        //去重
                        for (BluetoothDevice bd : listDevices) {
                            if (bd.getAddress().equalsIgnoreCase(bluetoothDevice.getAddress())) {
                                isSame = false;
                            }
                        }
                        if (isSame) {
                            listDevices.add(bluetoothDevice);
                            listDeviceName.add(new TieBean(strDevice));
                            // tieAdapter.notifyDataSetChanged();
                        }

                    }
                }
            });
        }
    };
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                //断开设备
                mBluetoothGatt.disconnect();
            } else if (msg.what == 2) {
                //连接设备
                if (btAdapt == null || !btAdapt.isEnabled() || btAdapt.getState() == BluetoothAdapter.STATE_OFF) {// 如果蓝牙还没开启
                    Toast.makeText(com.mobibrw.lego.MainActivity.this, "请先打开蓝牙", Toast.LENGTH_SHORT).show();
                    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                    startActivityForResult(enableBtIntent, 0);
                    return;
                }

                if (mScanning) {
                    btAdapt.stopLeScan(getmLeScanCallback);
                }
                listDevices.clear();
                listDeviceName.clear();
                mScanning = true;
                btAdapt.startLeScan(getmLeScanCallback); //开始搜索
                //定时器操作
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mScanning = false;
                        btAdapt.stopLeScan(getmLeScanCallback);
                        //其中BuildBean,TieAdapter是在build.gradle中dependencies {compile 'com.dou361.dialogui:jjdxm-dialogui:1.0.3'};
                        tieAdapter = new TieAdapter(mContext, listDeviceName, true);
                        BuildBean buildBean = DialogUIUtils.showMdBottomSheet(mActivity, true, "", listDeviceName, 0, new DialogUIItemListener() {
                            @Override
                            public void onItemClick(CharSequence text, int position) {
                                mBluetoothDevice = btAdapt.getRemoteDevice(listDevices.get(position).getAddress());
                                if (mBluetoothDevice == null) {
                                    return;
                                }
                                mBluetoothGatt = mBluetoothDevice.connectGatt(mActivity, false, mGattCallback);
                            }
                        });
                        if (listDevices.size() > 0) {
                            buildBean.mAdapter = tieAdapter;
                            buildBean.show();
                        } else {
                            Toast.makeText(com.mobibrw.lego.MainActivity.this, "未搜索到蓝牙设备", Toast.LENGTH_SHORT).show();
                        }
                    }
                }, 2000);    //延时2s执行
            } else if (msg.what == 3) {
                tv_seacher_ble.setText("断开蓝牙");
                bIsConnected = true;
                Toast.makeText(com.mobibrw.lego.MainActivity.this, "蓝牙连接成功", Toast.LENGTH_SHORT).show();
            } else if (msg.what == 4) {
                tv_seacher_ble.setText("连接蓝牙");
                bIsConnected = false;
                mBluetoothGatt.close();
                mBluetoothGatt = null;
            }
        }
    };);
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, final int status, final int newState) {
            LogUtil.e("StateChange,status=" + status + ",newState==" + newState + ",Services=" + mBluetoothGatt.discoverServices());
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {//2
                    LogUtil.e("去连接GATT server");
                } else if (newState == BluetoothProfile.STATE_CONNECTING) {
                    LogUtil.e("连接中");
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    LogUtil.e("已断开断开蓝牙Disconnected");
                    Message message = new Message();
                    message.what = 4;
                    handler.sendMessage(message);
                } else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
                    LogUtil.e("断开中");
                }
            } else if (status == 133) {
                //防止出现133,搜索到列表连接不上
                LogUtil.e("出现133");
                Message message = new Message();
                message.what = 5;
                handler.sendMessage(message);
            } else if (status == 8) {
                //自动断开
                LogUtil.e("自动断开了");
                Message message = new Message();
                message.what = 6;
                handler.sendMessage(message);
            } else if (status == 40) {
                //距离太远
                Message message = new Message();
                message.what = 7;
                handler.sendMessage(message);
            } else {
//其他连接不到设备的情况
            }
            Message message = new Message();
            message.what = 8;
            handler.sendMessage(message);
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            LogUtil.e("ServicesDiscovered状态status==" + status + ",Services==" + gatt.getServices());
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Message message = new Message();
                message.what = 3;
                handler.sendMessage(message);
                //激活要开启的服务
                displayGattServices(gatt.getServices());
                LogUtil.e("Discovered获取GATT_SUCCESS");
            } else {
                LogUtil.e("onServicesDiscovered received: " + status);
                bIsConnected = false;
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);//去掉标题栏
        setContentView(com.mobibrw.lego.R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//锁屏
        tv_seacher_ble = (TextView) findViewById(com.mobibrw.lego.R.id.tv_main_id);
        final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        btAdapt = bluetoothManager.getAdapter();
        //  btAdapt = BluetoothAdapter.getDefaultAdapter();// 初始化本机蓝牙功能,除了搜索方法欢了,获取蓝牙适配器的方法也换了
        //添加了震动
        vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        tv_seacher_ble.setOnClickListener(new View.OnClickListener() {
            {
                vibrator.vibrate(200);//震动指定时间
                //蓝牙搜索
                Message message = new Message();
                message.what = 2;
                handler.sendMessage(message);
            } else

            @Override
            public void onClick(View v) {
                final AlertDialog.Builder myDialog = new AlertDialog.Builder(com.mobibrw.lego.MainActivity.this);
                //nomalDialog.setIcon(R.drawable.icon_dialog);
                myDialog.setTitle("提示");
                myDialog.setMessage("是否断开蓝牙设备?");
                myDialog.setPositiveButton("是", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Message message = new Message();
                        message.what = 1;
                        handler.sendMessage(message);
                    }
                });
                myDialog.setNegativeButton("否", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {

                    }
                });
                myDialog.show();

            }
        }
    }

    //打开服务
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null)
            return;
        String uuid = null;
        for (BluetoothGattService gattService : gattServices) {
            uuid = gattService.getUuid().toString();
            List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();

            for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
                uuid = gattCharacteristic.getUuid().toString();
                if (uuid.equals(SPP_UUID)) {
                    final BluetoothGattCharacteristic characteristic = gattCharacteristic;
                    final int charaProp = characteristic.getProperties();
                    //这里根据charaProp 去做读、写、通知,真正的连接成功是在这里做判断的
                    // Message message = new Message();
                    //message.what = 3;
                    //  handler.sendMessage(message)
                }
            }
        }

    }

    @Override

    public void onDestroy() {

        super.onDestroy();

        if (mScanning) {

            btAdapt.stopLeScan(getmLeScanCallback);

        }

        if (NotNull.isNotNull(mBluetoothGatt)) {

            mBluetoothGatt.disconnect();

            mBluetoothGatt.close();

        }

        android.os.Process.killProcess(android.os.Process.myPid());

    }

}

            经过这样修改之后,奇迹又出现了,蓝牙搜索到确实快了很多,一秒左右甚至不到一秒就把蓝牙设备搜索出来了, 真是令人喜出望外,想起华为荣耀8那么奇葩,不知这个方法在华为荣耀8上测试如何,然而,2秒,4秒,8秒,10秒,20秒都在华为荣耀8搜索不出来,心又凉了半截,暗想这个方法看来还不适用,又白整了,于是又查各种资料,有的说Android系统6.0以上的手机需要定位权限,低头一想,还真是,一直测试的手机是Android系统6.0以下的,华为荣耀8是7.0以上的,抱着试试的态度,申请权限,还真是努力出奇迹,定位权限允许后,华为荣耀8确实也在不到一秒将蓝牙设备搜索出来,测试了其他6.0以上的手机,都正常很快搜出来。
        测试没有其他问题,打包发布,然而客户不给定位权限,问公司怎么一点搜索不出来,告知需要允许定位,才能搜索到,客户觉得一个蓝牙搜索你跟我要定位,很是不理解,老板也认为这样的体验也不好,客户不给权限,就无法使用,非常不友好。告知领导6.0以上必须给定位权限,否则搜不到, 这个没法啊,领导说你看第三方的蓝牙调试工具,在6.0以上的手机,不用打开定位,而且搜索的更快,第三方都能做到,你想办法吧, 我当然不苟同,下载了几个第三方蓝牙调试工具,有的确实要定位权限不允许定位就无法搜索到,有的确实禁止定位权限也能快速搜索到蓝牙设备,这么啪啪打脸,只能埋头去想办法了,同时,为了保证程序兼容性,公司又买了几部6.0以上的安卓手机,让我更惊心的是,魅族6和华为畅享7不仅要允许定位权限,GPS定位还要打开,GPS不打开,也是搜索不到蓝牙设备,想起以前开发的项目,由于安卓碎片化,不同的手机厂商对定位,GPS管理不一样,有的打开定位权限GPS权限也给了,有的打开GPS定位权限也给了,有的是他们两个独立,处理起来很麻烦,而蓝牙有的要GPS,有的不要GPS,6.0一下还不需要任何定位权限,想着这处理起来太麻烦,还是安心去找不要定位权限的方法吧,其实当时心里是没底的,虽然第三方已经实现这种不需要定位权限就能搜索到,心想,第三方也不知用的是什么黑科技,于是就查找Android系统6.0一上的手机不需要定位权限就能把蓝牙设备搜索出来等相关资料,其中最有帮助的查到 btAdapt.startLeScan(getmLeScanCallback) 这个方法已过时,Google已经在api里将其划掉,不推荐使用,推荐使用:

BluetoothLeScanner scanner = btAdapt.getBluetoothLeScanner();
scanner.startScan(scanCallback);

喜出过望,难道是用的方法过时了,所以需要定位权限,上代码:

ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            final BluetoothDevice bluetoothDevice = result.getDevice();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    boolean isSame = true;
                    if (bluetoothDevice != null && bluetoothDevice.getName() != null) {
                        LogUtil.e("蓝牙设备==" + bluetoothDevice.getName() + bluetoothDevice.getAddress());
                            strDevice =bluetoothDevice.getName() + "|" + bluetoothDevice.getAddress();
                            for (BluetoothDevice bd : listDevices) {
                                if (bd.getAddress().equalsIgnoreCase(bluetoothDevice.getAddress())) {
                                    isSame = false;
                                }
                            }
                            if (isSame) {
                                listDevices.add(bluetoothDevice);
                                listDeviceName.add(new TieBean(strDevice));
                                if (bleDeviceAdapter != null) {
                                    bleDeviceAdapter.notifyDataSetChanged();
                                }
                            }
 
 
                    }
                }
            });
        }
 
        @Override
        public void onBatchScanResults(final List<ScanResult> results) {
            super.onBatchScanResults(results);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    LogUtil.d("扫描onBatchScanResults==" + results.size());
                }
            });
 
        }
 
        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
            LogUtil.e("扫描失败onScanFailed, errorCode==" + errorCode);
        }
    };

停止扫描的方法是:

scanner.stopScan(scanCallback);

        然而,虽然使用了新方法,速度也是一样快,但是还是需要定位权限,魅族6和华为畅享7还得打开GPS,看来这个也不是正解,当然项目中的扫描外围蓝牙设备的方法也都换成了scanner.startScan(scanCallback);
第一天毫无结果,灰头土脸,昏天暗地,焦头烂额地回家。
第二天继续查找相关资料,在一个偶然的机会,看到一篇博客说,把app中的build.gradle中的:

 defaultConfig {
        applicationId "com.demo.ble"
        minSdkVersion 18
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

中的  targetSdkVersion 25改成 targetSdkVersion 22,就不需要定位权限也可以搜索到蓝牙设备了,看到网上很少有人这样说的,搜了那么多资料,就一篇这样说的,带着半信半疑将targetSdkVersion改成 22, 编译,也没报任何错,运行,还真是又出奇迹,测试了手上的所有手机,都不需要定位,不要GPS就可以搜索到蓝牙设备。
       至于为什么会这样,可以查找 Android compileSdkVersion、buildToolsVersion,targetSdkVersion之间的区别,网上有很多解释,保证能解答你的疑惑,下面附上一些简单的解释
     targetSDKVersion
    简单来说就代表着你的App能够适配的系统版本,意味着你的App在这个版本的手机上做了充分的 前向 兼容性处理和实际测试。其实我们写代码时都是经常干这么一件事,就是 if(Build.VERSION.SDK_INT >= 23) { ... } ,这就是兼容性处理最典型的一个例子。如果你的target设置得越高,其实调用系统提供的API时,所得到的处理也是不一样的,甚至有些新的API是只有新的系统才有的Android6.0普通权限normal permission 和 危险权限dangerous permission 。
 Normal Permission:写在xml文件里,那么App安装时就会默认获得这些权限,即使是在Android6.0系统的手机上,用户也无法在安装后动态取消这些normal权限,这和以前的权限系统是一样的,不变。
Dangerous Permission:还是得写在xml文件里,但是App安装时具体如果执行授权分以下几种情况:
1、targetSDKVersion < 23 & API(手机系统) < 6.0 :安装时默认获得权限,且用户无法在安装App之后取消权限。
 2、targetSDKVersion < 23 & API(手机系统) >= 6.0 :安装时默认获得权限,但是用户可以在安装App完成后动态取消授权( 取消时手机会弹出提醒,告诉用户这个是为旧版手机打造的应用,让用户谨慎操作 )。
3、targetSDKVersion >= 23 & API(手机系统) < 6.0 :安装时默认获得权限,且用户无法在安装App之后取消权限。
 4、targetSDKVersion >= 23 & API(手机系统) >= 6.0 :安装时不会获得权限,可以在运行时向用户申请权限。用户授权以后仍然可以在设置界面中取消授权,用户主动在设置界面取消后,在app运行过程中可能会出现crash。
其他:
        1.虽然解决了大问题,但是目前项目中发现,连续扫描-断开-扫描四五次,就无法再搜索到外围蓝牙设备,尤其是在每次搜索1-2秒内扫描断开,4、5次无论如何也是搜索不到,需要重新进入界面才能扫描到,每次扫描花费在4、5秒以上的,出现4、5次扫不到的几率就很低了,当然,4、5秒是老板和客户无法忍受的。
2.蓝牙连接成功自动断开;

参考链接


Android dp 和 CSS px

最近在给`WebView`前端传递标题栏高度的时候,传递的是实际像素`Pixel`,结果前端直接设置这个数值的时候,发现太高了。搜索了一下,发现前端的`CSS`样式设置的像素值是逻辑像素,这个逻辑像素跟实际像素是不同的,对于`Android`来说,就是设备的`dp`数值。

参考文章如下:

前情提要:设计师给的设计稿是1242分辨率的(iPhone6p),标注的字体大小是54px,前端工程师写的H5页面是18px,效果正常且能自适应iPhone6。来调查一下 为什么是标注数值除以3?为什么自动适应iPhone6?css里的px和Android的dp有怎样的关系?

在进行具体的分析之前,首先得知道下面这些关键性基本概念(术语)。

物理像素(physical pixel)

一个物理像素是显示器(手机屏幕)上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。

设备独立像素(density-independent pixel)

设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素。

所以说,物理像素和设备独立像素之间存在着一定的对应关系,这就是接下来要说的设备像素比

设备像素比(device pixel ratio)

设备像素比(简称dpr)定义了物理像素和设备独立像素的对应关系,它的值可以按如下的公式的得到:

设备像素比 = 物理像素 / 设备独立像素 // 在某一方向上,x方向或者y方向

在javascript中,可以通过window.devicePixelRatio获取到当前设备的dpr。

在css中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。


iPhone6的物理像素是750 x 1334,设备像素比(devicePixelRatio)是2,设备独立像素是375 x 667 (750/2 x 1334/2)

iPhone6p的物理像素是1080 x 1920,但logic pixel是1242 x 2208, 设备像素比(devicePixelRatio)是3,设备独立像素是414 x 736 (1242/3 x 2208/3)

css里面有物理像素逻辑像素的概念,在这里,逻辑像素可以理解为设备独立像素

其实,logic point更适合翻译成逻辑点。无论是逻辑像素还是逻辑点,只需要理解一个逻辑点(逻辑像素)需要一个或一个以上的物理像素来展示就可以了。

那么,为什么iPhone6p是这样奇葩的逻辑点呢?

  • 如果逻辑点分辨率用 360x640, 360x640@3x 正好是 1080x1920。但是逻辑pt分辨率 360x640 就会比 iPhone 6的 375x667 还低,也就是说相同字号的情况下,iPhone 6如果一行显示了25个字,而 iPhone 6 Plus按这个逻辑pt方案,一行就会只能显示24 个字了。
  • 如果逻辑点分辨率用 540x960,540x960@2x 正好是 1080x1920。但是iOS UI 元素尺寸在屏幕上的实际物理面积一下子就变小了,比如标签栏或导航栏按钮的物理高度只有原来的 81.5% ,点击面积就只有iPhone 6 的0.815*0.815=66.4%,用户点击就困难了。
  • 如果物理分辨率做到1242x2208就减少了一个从1242压缩到1080的过程,但是1242的物理分辨率是在1080p和2k屏之前的尺寸,功耗和成本都会提升,而且这样非1440的2k分辨率的屏幕采购也是问题。

至于为什么一定是 414x736,有人估计应该是在 5.5inch 和 ppi=461 这两个前提限定的情况下,按这个 414x736 pt 分辨率,屏幕上 UI 元素操作物理大小最接近 iPhone 6上的表现。


Android世界中更加复杂,这里列举了几款手机的基本数据

  Nexus 4 Nexus 6 Nexus 6p LG L24 Mi 4c
物理像素 768 x 1280 1440 x 2560 1440 x 2560 1440 x 2560 1080 x 1920
设备像素比(dpr) 2 3.5 3.3 4 3
设备独立像素(dip/dp) 384 x 640 412 x 732 435 x 773 360 x 640 360 x 640

Android提供了获取density的方法:

float density = context.getResources().getDisplayMetrics().density;

density的官方定义是The logical density of the display,翻译过来是屏幕的逻辑密度,其实就是之前提到的设备像素比(dpr)


在浏览器中,获取设备像素比就比较简单:

window.devicePixelRatio

比如,我写了一个网页,用手机浏览器打开:

<button onclick="alert('window.devicePixelRatio='+window.devicePixelRatio);">show devicePixelRatio</button>

结果显示与Android的density的结果是一致的。

注:<meta name="viewport" content="width=device-width, initial-scale=1">
By specifying width=”device-width”, you are asking the browser to apply a scaling factor to its screen pixels. One css pixel occupies one or more screen pixels. How many more? This value is called the css pixel ratio.


回顾一下

同一台设备,它的设备像素比(dpr)是确定的,无论通过Android api 获取还是浏览器 js/css 获取。
The css px is actually Device Independent Pixel(dip), and it is a common pattern to use px as dp in css.
css中的px具有与Android中的dp等同的效果

参考链接


Python发送form-data请求提交表单数据

最近在处理服务器接口的时候,需要使用`Python`模拟发送表单数据到服务器,搜索了一下,找到如下例子:

# encoding=utf8
import urllib
import urllib2
import httplib
import os

HOST = 'xx.xx.xx.xx'
PORT = 8000
P_ID = xx
U_NAME = 'xxx'
API_KEY = 'xxx'
API_SEC = 'xxx'
UP_APK = xx

def sign(src, key):
  HEX_CHARS = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ]
  from hashlib import sha1
  import hmac
  hashed = hmac.new(key, src, sha1)
  sv = str(hashed.digest())
  ret = ''
  for b in bytearray(sv):
    bite = b & 0xff
    ret += HEX_CHARS[ bite >> 4 ]
    ret += HEX_CHARS[ bite & 0xf ]
  return ret

def encode_multipart_formdata(fields, files):
  """
  fields is a sequence of (name, value) elements for regular form fields.
  files is a sequence of (name, filename, value) elements for data to be uploaded as files
  Return (content_type, body) ready for httplib.HTTP instance
  """
  import random
  BOUNDARY = '----------%s' % ''.join(random.sample('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ', 62))
  CRLF = '\r\n'
  L = []
  for (key, value) in fields:
    L.append('--' + BOUNDARY)
    L.append('Content-Disposition: form-data; name="%s"' % key)
    L.append('')
    L.append(value)
  for (key, filename, value) in files:
    L.append('--' + BOUNDARY)
    L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
    L.append('Content-Type: %s' % get_content_type(filename))
    L.append('')
    L.append(value)
  L.append('--' + BOUNDARY + '--')
  L.append('')
  body = CRLF.join(L)
  content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
  return content_type, body, BOUNDARY

def get_content_type(filename):
  import mimetypes
  return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

def post_apk_sign(apk_file):
  filename = os.path.basename(apk_file)
  url = 'http://' + HOST + ':' + str(PORT) + 'xxx'
  fields = [('un', U_NAME), ('p_id', str(P_ID)), ('up_type', str(UP_APK))]
  with open(apk_file, 'r') as f:
    files = [('apk_file', filename, f.read())]
  content_type, body, boundary = encode_multipart_formdata(fields, files)
  si = sign(API_KEY + str(P_ID) + str(UP_APK) + U_NAME, API_SEC)
  # print si
  req = urllib2.Request(url)
  req.add_header('api_key', API_KEY)
  req.add_header('sign', si)
  req.add_header('Content-Type', 'multipart/form-data; boundary='+boundary)
  resp = urllib2.urlopen(req, body)
  res = resp.read()
  print res

if __name__=='__main__':
  # b38d73bb278800dd3c0c3cf11ad20c106d17944c
  # print(sign(b'123test123456', b'456'))

  apk_file = 'xxx.apk'
  post_apk_sign(apk_file)

参考链接


rsync同步报告错误"cannot delete non-empty directory"

最近在执行`rsync`的时候,发现报告错误信息`cannot delete non-empty directory`,如下:

$ rsync -avzpP --delete -e "ssh -p $PORT" $USER@$SERVER:$WWW_DIR $WWW_DST
cannot delete non-empty directory: xxxxx
cannot delete non-empty directory: xxxxx
cannot delete non-empty directory: xxxxx

解决方法是增加`--delete-excluded`即可,如下:

$ rsync -avzpP --delete --delete-excluded -e "ssh -p $PORT" $USER@$SERVER:$WWW_DIR $WWW_DST

参考链接