Podcast 托管

Podcast 近两年又突然火起来,一度认为 Podcast 是最近才流行起来的媒体,后来才发现这种媒介早在上世纪 80 年代就诞生 。近两年被再一次提起可能 iOS 系统内置播客系统是个契机,一时间让 Podcast 进入大众的视听,而国内近些年也陆陆续续出现了很多播客平台,喜马拉雅,荔枝等等, 播客这种形式在播客,视频播客新奇之后似乎很难找到自己的定位,但是在日常生活中还是有很多情况适用播客,在开车等双手双眼需要时刻准备着的时候,播客系统可以可以代替收音机,而在长途跋涉需要闭目养神时,播客也可以成为音乐的代替。

但是经过这么多年的演进, Podcast 还是依赖于 RSS 2.0 标准,不论是 iTunes 还是 Play Music ,虽然 Google 已经用关闭 Google Reader 来表明对 RSS 的态度,却还是依然无法将 Podcast 从 RSS 剥离。其实这也很矛盾,就像之前看到的一则评论,Google 托管了最大的视频播客 YouTube,却连小小的音频文件都懒得托管。所以无奈才有了此篇文章。

Podcast Hosting

有几种免费靠谱的托管方式,这里就不讲收费的方案了,因为免费的可以做到很好。个人使用完全没有问题。

  • Dropbox, Dropbox 导出直链自行 Google
  • Google Drive,如何导出直链自行 Google
  • SoundCloud,有高级版
  • Internet Archive
  • 最后 Cloudup 我使用的方案,支持 1000 个文件托管,具体可参看之前总结的一篇文章

其实苹果官网也给了一个 Podcast Hosting 的列表 可以查看。

Generate RSS feed

iTunes 和 Google Play Music 都需要不同格式的 RSS,需要特别考虑。另外音频最好带封面 14001400 px 到 20482048 px 大小,这个是 iTunes Store 需要的。

iTunes 需要的标签

iTunes 中关于 Podcast 的介绍 。 每一个 Podcast 叫做 Episode,所有独立的可下载的音频,视屏,PDF,ePub,都属于 Episode。 iTunes 支持的文件格式包括: M4A,MP3,MOV,MP4,M4V,PDF,ePub 等等。通过 Feed Validation Service 验证 Feed 有效性。以下例子可以在帮助文档 中查看到。

iTunes 中需要的 Feed 文件格式:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
    <title>All About Everything</title>
    <link>http://www.example.com/podcasts/everything/index.html</link>
    <language>en-us</language>
    <copyright>&#x2117; &amp; &#xA9; 2014 John Doe &amp; Family</copyright>
    <itunes:subtitle>A show about everything</itunes:subtitle>
    <itunes:author>John Doe</itunes:author>
    <itunes:summary>All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our podcast in the Podcasts app or in the iTunes Store</itunes:summary>
    <description>All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our podcast in the Podcasts app or in the iTunes Store</description>
    <itunes:owner>
    <itunes:name>John Doe</itunes:name>
    <itunes:email>john.doe@example.com</itunes:email>
    </itunes:owner>
    <itunes:image href="http://example.com/podcasts/everything/AllAboutEverything.jpg"/>
    <itunes:category text="Technology">
    <itunes:category text="Gadgets"/>
    </itunes:category>
    <itunes:category text="TV &amp; Film"/>
    <itunes:category text="Arts">
    <itunes:category text="Food"/>
    </itunes:category>
    <itunes:explicit>no</itunes:explicit>
    <item>
        <title>Shake Shake Shake Your Spices</title>
        <itunes:author>John Doe</itunes:author>
        <itunes:subtitle>A short primer on table spices</itunes:subtitle>
        <itunes:summary><![CDATA[This week we talk about <a href="https://itunes/apple.com/us/book/antique-trader-salt-pepper/id429691295?mt=11">salt and pepper shakers</a>, comparing and contrasting pour rates, construction materials, and overall aesthetics. Come and join the party!]] ></itunes:summary>
        <itunes:image href="http://example.com/podcasts/everything/AllAboutEverything/Episode1.jpg"/>
        <enclosure length="8727310" type="audio/x-m4a" url="http://example.com/podcasts/everything/AllAboutEverythingEpisode3.m4a"/>
        <guid>http://example.com/podcasts/archive/aae20140615.m4a</guid>
        <pubDate>Tue, 08 Mar 2016 12:00:00 GMT</pubDate>
        <itunes:duration>07:04</itunes:duration>
        <itunes:explicit>no</itunes:explicit>
    </item>
    <item>
        <title>Socket Wrench Shootout</title>
        <itunes:author>Jane Doe</itunes:author>
        <itunes:subtitle>Comparing socket wrenches is fun!</itunes:subtitle>
        <itunes:summary>This week we talk about metric vs. Old English socket wrenches. Which one is better? Do you really need both? Get all of your answers here.</itunes:summary>
        <itunes:image href="http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg"/>
        <enclosure length="5650889" type="video/mp4" url="http://example.com/podcasts/everything/AllAboutEverythingEpisode2.mp4"/>
        <guid>http://example.com/podcasts/archive/aae20140608.mp4</guid>
        <pubDate>Wed, 09 Mar 2016 13:00:00 EST</pubDate>
        <itunes:duration>04:34</itunes:duration>
        <itunes:explicit>no</itunes:explicit>
    </item>
    <item>
        <title>The Best Chili</title>
        <itunes:author>Jane Doe</itunes:author>
        <itunes:subtitle>Jane and Eric</itunes:subtitle>
        <itunes:summary>This week we talk about the best Chili in the world. Which chili is better?</itunes:summary>
        <itunes:image href="http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg"/>
        <enclosure length="5650889" type="video/x-m4v" url="http://example.com/podcasts/everything/AllAboutEverythingEpisode2.m4v"/>
        <guid>http://example.com/podcasts/archive/aae20140697.m4v</guid>
        <pubDate>Thu, 10 Mar 2016 02:00:00 -0700</pubDate>
        <itunes:duration>04:34</itunes:duration>
        <itunes:explicit>no</itunes:explicit>
        <itunes:isClosedCaptioned>Yes</itunes:isClosedCaptioned>
    </item>
    <item>
        <title>Red,Whine, &amp; Blue</title>
        <itunes:author>Various</itunes:author>
        <itunes:subtitle>Red + Blue != Purple</itunes:subtitle>
        <itunes:summary>This week we talk about surviving in a Red state if you are a Blue person. Or vice versa.</itunes:summary>
        <itunes:image href="http://example.com/podcasts/everything/AllAboutEverything/Episode4.jpg"/>
        <enclosure length="498537" type="audio/mpeg" url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"/>
        <guid>http://example.com/podcasts/archive/aae20140601.mp3</guid>
        <pubDate>Fri, 11 Mar 2016 01:15:00 +3000</pubDate>
        <itunes:duration>03:59</itunes:duration>
        <itunes:explicit>no</itunes:explicit>
    </item>
</channel>
</rss>

Play Music 需要的标签

完整的字段及解释可以参考官方文档,里面有非常详细的字段解释。Play Music 要求的大多数内容和 iTunes 类似,只有一些字段要求有些变化,Google 要求图片为方形,最小为 600*600 ,并且建议大小在 1200*12007000*7000 px 之间,支持 JPEG 和 PNG。

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" version="2.0">
<channel>
    <title>The Unknown Podcast</title>
    <link>http://sample.com/podcasts/unknown/index.html</link>
    <language>en-us</language>
    <copyright>℗ 2025 Unknown Podcaster Corp</copyright>
    <googleplay:author>Unannounced Podcaster</googleplay:author>
    <googleplay:description>The Unknown Podcast will look at all the things that are unknown or unknowable. Find us on Google Play Music!</googleplay:description>
    <description>The Unknown Podcast will look at all the things that are unknown or unknowable.</description>
    <googleplay:email>unknown-podcast@sample.com</googleplay:email>
    <googleplay:image href="http://sample.com/podcasts/unknown/UnknownLargeImage.jpg" />
    <googleplay:category text="Technology"/>
    <item>
        <title>What's out there?</title>
        <googleplay:author>Engima</googleplay:author>
        <googleplay:description>We look at all the things that are out there that we'd like to know.</googleplay:description>
        <googleplay:image href="http://sample.com/podcasts/unknown/Episode1.jpg" />
        <enclosure url="http://sample.com/podcasts/UnknownPodcastEpisode1.mp3" length="2320111" type="audio/mpeg" />
        <guid>http://sample.com/podcasts/UnknownPodcastEpisode1.mp3</guid>
        <pubDate>Mon, 29 Jun 2015 19:00:00 GMT</pubDate>
    </item>
    <item>
        <title>What can we know, really?</title>
        <googleplay:author>The everyman</googleplay:author>
        <googleplay:description>And then we follow up on last week's podcast to examine what can and cannot be known.</googleplay:description>
        <googleplay:image href="http://sample.com/podcasts/unknown/Episode2.jpg" />
        <enclosure url="http://sample.com/podcasts/UnknownPodcastEpisode2.m4a" length="6421543" type="audio/x-m4a" />
        <guid>http://sample.com/podcasts/UnknownPodcastEpisode2.m4a</guid>
        <pubDate>Mon, 6 Jul 2015 19:00:00 GMT</pubDate>
    </item>
</channel>
</rss>

提交认证

可以向以下两个网址提交申请认证

最后可以从这里访问到网站 https://einverne.github.io/podcast ,网站源代码在 https://github.com/einverne/podcast

Demo 的 Play Music 地址在 这里

reference


2016-10-17 podcast , google , itunes

Head First Design Patterns

Some of the Head First learning principles:

  • make it visual.
  • use a conversational and personalized style.
  • think more deeply.
  • keep attention.
  • touch emotions.

To bend your brain into submission

  • Slow down, the more you understand, the less you have to memorize.
  • Do the exercises, write your own notes.
  • Make this the last thing you read before bed.
  • Talk about it. Out loud.
  • Pay attention to whether your brain is getting overloaded. Take a break if necessary.
  • Feel something, make up your own captions for the photos. Even bad jokes better then feeling nothing at all
  • Design something new. Apply new knowledeg in your new desigin or refactor an old project.

Principle

Identify the aspects of your application that vary and separate them from what stays the same.

The what varies and “encapsulate” it so it won’t affect the rest of your code.

Program to an interface really means “Program to a supertype”

The Open-Closed Principle

开闭原则

Classes should be open for extension, but close for modification.

第一次听到开闭原则的时候可能会有些疑惑,但是这个原则其实是让架构容易扩展的同时不影响到原来的代码。比如观察者模式,我们可以轻松的在不修改被观察者代码的情况下,通过增加观察者来实现对系统的扩展。

开闭原则下的模式:

  • Observer 模式
  • 工厂模式

2016-10-17 design-pattern , java , learning-notes

xiaomi router samba password

ssh to xiaomi router

smbpasswd -a root

then enter your password two times.

vi /etc/config/samba

Edit

option 'guest_ok'       no
option 'force_user'     'root'

And

vi /var/etc/smb.conf

Edit

guest ok = yes
valid users = root

Then restart samba service:

/etc/init.d/samba restart

2016-10-11 xiaomi , router , samba , linux

Android 提醒

Android 对话框

实现对话框通常有三种方式:

  • 使用 Dialog 类,或者其派生类 每个类被用来提供特定功能,比如日期选择,单选等等
  • 对话框主题的 Activity 可以将对话框主题应用到 Activity 上,使 Activity 外观类似于对话框
  • Toast 特殊的,短暂的,非模态的消息对话,通常在 Broadcast Receiver 和 Service 中使用,来提示用户响应事件

以下重点考虑 Dialog 类的使用,其派生子类 DatePickerDialog,TimePickerDialog 以后扩展开讲。响应代码可以参考此次 提交。

最常见的对话框 AlertDialog

private void showNormalDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Title");
    builder.setMessage("your message here.");
    builder.setCancelable(true);

    /**
    * Typically, a dialog is dismissed when its job is finished
    * and it is being removed from the screen.
    * A dialog is canceled when the user wants to escape the dialog
    * and presses the Back button.
    * For example, you have a standard Yes/No dialog on the screen.
    * If the user clicks No, then the dialog is dismissed
    * and the value for No is returned to the caller.
    * If instead of choosing Yes or No, the user clicks Back to escape the dialog
    * rather than make a choice then the dialog is canceled
    * and no value is returned to the caller.
    */
    builder.setPositiveButton(
            "Yes",
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    Toast.makeText(DialogTestActivity.this, "Yes", Toast.LENGTH_SHORT).show();
                    dialog.dismiss();

                }
            });

    builder.setNegativeButton(
            "No",
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    Toast.makeText(DialogTestActivity.this, "No", Toast.LENGTH_SHORT).show();
                    dialog.dismiss();
                }
            });

    AlertDialog alert = builder.create();
    alert.show();
}

三选对话框

private void showThreeOptionsDialog() {
    Dialog dialog = new AlertDialog.Builder(this)
            .setIcon(android.R.drawable.btn_star)
            .setTitle("喜好调查")
            .setMessage("你喜欢李连杰的电影吗?")
            .setPositiveButton("很喜欢", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Toast.makeText(DialogTestActivity.this, "我很喜欢他的电影。",
                            Toast.LENGTH_LONG).show();
                }
            })
            .setNegativeButton("不喜欢", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Toast.makeText(DialogTestActivity.this, "我不喜欢他的电影。", Toast.LENGTH_LONG).show();
                }
            })
            .setNeutralButton("一般", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Toast.makeText(DialogTestActivity.this, "谈不上喜欢不喜欢。", Toast.LENGTH_LONG).show();
                }
            }).create();
    dialog.show();
}

带输入框的对话框

private void showInputDialog() {
    final EditText editText = new EditText(this);
    Dialog dialog = new AlertDialog.Builder(this)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setTitle("Input your message")
            .setView(editText)
            .setPositiveButton("Sure", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    String inputString = editText.getText().toString();
                    Toast.makeText(DialogTestActivity.this, inputString, Toast.LENGTH_SHORT).show();
                }
            })
            .setNegativeButton("No", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            }).create();
    dialog.show();
}

单选对话框

private void showSingleChoiceDialog() {
    Dialog dialog = new AlertDialog.Builder(this)
            .setTitle("Single Choice")
            .setSingleChoiceItems(new String[]{"item1", "item2"}, 0, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Toast.makeText(DialogTestActivity.this, "" + which, Toast.LENGTH_SHORT).show();
                    dialog.dismiss();
                }
            }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            }).create();
    dialog.show();
}

复选对话框

private void showMultiChoiceDialog() {
    Dialog dialog = new AlertDialog.Builder(this)
            .setTitle("Multi Choice")
            .setMultiChoiceItems(new String[]{"item0", "item1"}, new boolean[]{false, true}, new DialogInterface.OnMultiChoiceClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which, boolean isChecked) {

                    Toast.makeText(DialogTestActivity.this, "" + which + " " + isChecked, Toast.LENGTH_SHORT).show();
                }
            }).create();
    dialog.show();
}

自定义布局对话框

private void showCustomDialog() {
    LayoutInflater inflater = getLayoutInflater();
    View layout = inflater.inflate(R.layout.dialog_custom, null);
    final EditText editText = (EditText) layout.findViewById(R.id.editText);
    Dialog dialog = new AlertDialog.Builder(this)
            .setTitle("Custom")
            .setView(layout)
            .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Toast.makeText(DialogTestActivity.this, editText.getText().toString() + " ", Toast.LENGTH_SHORT).show();
                }
            }).create();
    dialog.show();
}

reference


2016-10-11 Android , AndroidDev

Android 使用自定义 keystore 调试

可以在 build.gradle 文件中指定debug 下的 keystore 文件路径,一般放到项目跟目录下,并使用如下方式指定。 更多语法可以参考 Signing Configurations 部分 http://tools.android.com/tech-docs/new-build-system/user-guide

指定 debug variant 下 keystore 路径:

android {
    signingConfigs {
        debug {
            storeFile file('your.keystore')
            keyAlias 'androiddebugkey'
            keyPassword 'android'
            storePassword 'android'
        }
    }
}

推荐还是讲 debug 和 release 下的 keystore 分开。

保存一些和 keystore 相关的命令

  1. 查询 keystore 中条目

    keytool -list -keystore your.keystore -storepass yourpassword

  2. 修改 keystore 中,使用上一步获取到的证书名字

    keytool -changealias -alias 证书名字 -destalias androiddebugkey -keystore your.keystore -storepass yourpassword

  3. 修改密钥库密码

    keytool -storepasswd -keystore your.keystore -storepass yourpassword

  4. 修改证书密码

    keytool -keypasswd -alias androiddebugkey -keystore 证书名字 -storepass yourpassword

reference


2016-10-09 Android , AndroidDev

全平台录制 GIF:LICEcap

以前写过一篇文章 讲的是如何在 Linux 下使用 byzanz 来录制 GIF,并且能够达到很好的效果,并且文件大小非常合适网络分享。现在就介绍下在其他两大平台 Win/Mac 下录制 GIF 的工具。

LICEcap

录屏 GIF 工具名字叫做: LICEcap 。这也是一款我在使用很长时间之后感觉非常好用的工具。他的官网地址:

http://www.cockos.com/licecap/

他有如下优点:

  • 开源,简单,小巧
  • 支持直接录屏转 GIF,或者自由 .LCF 格式,使用 REAPER 可以转制成 .gif 或者其他视频格式
  • 支持录制中移动录制窗口
  • 支持暂停和继续
  • 全局快捷键 shift + space 可以暂停继续

更多其他的技巧和优点可查看他官网,目前为止他满足了我所有的需求。

使用:

record

输出文件:

output

ScreenToGif

在寻找的过程中还发现一款比较好用的工具,但只有在 Windows 平台上存在,这里也写上吧。

官方地址:https://screentogif.codeplex.com/

  • 同样支持直接输出 Gif
  • 支持调整大小

更多的介绍和使用说明可上官网查看。


2016-10-01 gif , 经验总结 , record , tools

通过 IFTTT 自动下载 Instagram 图片到 Google Drive

在 Instagram 关闭 API 之前可以通过 IFTTT 获取别人的更新 Photos, 但是 Instagram 收紧了 API 政策。既拿不到别人更新的信息流,同样也自己Like 别人照片的信息也拿不到了,原来 IFTTT 有两个 Recipes:

  • 一个为自动下载个人 Liked 别人的 Photo 到 Dropbox
  • 另一个为自动下载其他 ID 的更新 Photo

然而这两个 Recipes 都被 IFTTT 删去了,我甚至在 StackOverflow 上问过这件事情,只是几个月过去了,也没有任何实质性的方案。

直到这些天,突然脑袋一道闪光,再此之前,我了解了方法可以导出 Instagram 到 RSS,然后看到了别人自动将 Imgur 中的图片上传到 Google Drive ,使用的是 RSS 导出图片,有人写了一脚本传入 URL,就可以提取 URL 中的图片链接。

正是基于这两个方案,我想到了使用 Instagram to RSS to IFTTT to Google Drive 的方案,然后经过尝试,不需要一行代码的情况下,我实现了自动转存的方案。省去了自己写脚本的时间,同样这个方案也自动适配的Instagram 的网页,并不会因为网页结构的变化而导致失败。只要 RSS 有效,那么就会一直生效。

Instagram to RSS

要做到这个事情,就需要借助 RSS bridge 这个项目,这个项目也是当时我在寻找 InoReader RSS 的时候发现的,关于导出微博、知乎、微信的订阅到 RSS 可以以后在展开详谈。回到 RSS bridge ,这个项目本身就是利用爬虫将网站更新内容生成 RSS,本身支持的网站还是很多的,Flickr、GooglePlus、Twitter、Youtube、Pinterest 等等,当然包括 Instagram。

而我使用了 https://bridge.suumitsu.eu/ 这个网站提供的服务,这个网站架设了 RSS bridge 的服务,当然如果有条件的话自己架设也是很不错的选择,只要一直维护就可以。在网页上选择 Instagram 然后填入 Instagram 的用户 ID ,然后获取 Atom 的源即可。拿到这个 Feed URL,在下一步使用。

if RSS to Google Drive

拿到 Feed URL 之后,到 IFTTT,使用 这个 新建一个 Recipe

分析一下刚刚拿到的 URL

https://bridge.suumitsu.eu/?action=display&bridge=Instagram&u=instagram&format=Atom

其中 u 参数后面跟随着的就是 Instagram 的用户 ID,改变 u 后面的参数为需要自动保存的 ID,然后 Save 即可。


2016-09-30 经验总结 , Instagram , Google Drive , RSS , IFTTT

Android 常见错误

INSTALL_FAILED_NO_MATCHING_ABIS 的解决办法

出现时机

INSTALL_FAILED_NO_MATCHING_ABIS 的解决办法,在 Android 模拟器上安装 apk 的时候出现

解决办法

是由于使用了 native libraries ,该 native libraries 不支持当前的cpu的体系结构。

INSTALL_FAILED_NO_MATCHING_ABIS is when you are trying to install an app that has native libraries and it doesn't have a native library for your cpu architecture. For example if you compiled an app for armv7 and are trying to install it on an emulator that uses the Intel architecture instead it will not work.

现在安卓模拟器的CPU/ABI一般有三种类型,INTEL X86,ARM,MIPS,

如果选择用INTEL X86出现 INSTALL_FAILED_NO_MATCHING_ABIS 的错误,那就改用ARM的

参考:http://stackoverflow.com/questions/24572052/install-failed-no-matching-abis-when-install-apk

Apache HTTP Client

Android 6.0 中移除 了 Apache HTTP client

程序包org.apache.http.client.methods不存在

android {
    useLibrary 'org.apache.http.legacy'
}

2016-09-29 Android , AndroidDev

android 6 runtime permission

在 target API 23 之前,应用申请权限为一次性给予,开发者需要在 Manifest 中使用 users-permission 来申请权限,而用户则是在安装应用时一次性赋予应用所有申请的权限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.app.myapp" >
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

而在 Android API Level 23 也就是 Android 6.0 以后权限的问题被进一步细化,开发者可以在运行时申请权限(Runtime permissions),而此时众多的Android 权限也被细分为 Normal 普通权限 和 Dangerous 危险权限,普通权限和 6.0 以前一样在 Manifest 中申请,并且在安装应用时一次性赋予,而危险权限的申请则需要额外的注意。否则可能会引发异常

java.lang.SecurityException: Permission Denial

所有的权限列表在官方文档 可以查到,每一个权限都标明了 Protection level: normal or dangerous.

运行时获取权限

申请照相权限例子:

/**
 * Requests the Camera permission.
 * If the permission has been denied previously, a SnackBar will prompt the user to grant the
 * permission, otherwise it is requested directly.
 */
private void requestCameraPermission() {
    Log.i(TAG, "CAMERA permission has NOT been granted. Requesting permission.");

    // BEGIN_INCLUDE(camera_permission_request)
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.CAMERA)) {
        // Provide an additional rationale to the user if the permission was not granted
        // and the user would benefit from additional context for the use of the permission.
        // For example if the user has previously denied the permission.
        Log.i(TAG,
                "Displaying camera permission rationale to provide additional context.");
            Snackbar.make(mLayout, R.string.permission_camera_rationale,
                    Snackbar.LENGTH_INDEFINITE)
                    .setAction(R.string.ok, new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[]{Manifest.permission.CAMERA},
                                    REQUEST_CAMERA);
                        }
                    })
                    .show();
    } else {

        // Camera permission has not been granted yet. Request it directly.
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
                REQUEST_CAMERA);
    }
    // END_INCLUDE(camera_permission_request)
}

重要方法

检查权限

ContextCompat 中

public static int checkSelfPermission (Context context, String permission)

检查是否拥有权限,如果有返回 PackageManager PERMISSION_GRANTED, 如果没有则返回 PERMISSION_DENIED

在 ActivityCompat 中

public static boolean shouldShowRequestPermissionRationale (Activity activity, String permission)

在 UI 中弹出对话框申请权限,仅仅只有当当前功能需要权限的时候才需要申请。需要参数,目标 activity ,和需要申请的权限,返回是否需要弹出对话框。该方法监测是否需要申请权限。

申请权限

public static void requestPermissions (Activity activity, String[] permissions, int requestCode)

申请权限的权限需要在 manifest 中定义,权限需要是危险权限 #PROTECTION_DANGEROUS dangerous

普通权限 PROTECTION_NORMAL 会在安装时一次性授予, 同样 PROTECTION_SIGNATURE 权限也会在安装时授予。

定义 signature 权限时,不仅需要添加权限说明,还需要相同的签名。

如果app不拥有申请的权限,在用户接受或者拒绝之后,会收到一个回调,说明是否授予了权限,需要实现接口。

public abstract void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)

因为申请权限不能保证被授予,所以无论在有没有权限的情况下都要保证app能够运行。

requestPermissions 方法会开始一个 activity 来让用户选择是否授予权限,因此程序自身 Activity 可能会 paused 或者 resumed。进一步,授予某些权限可能会导致重启应用,这种情况下系统会重新生成 activity stack ,之后再调用 onRequestPermissionsResult 。

回调方法 onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 的第一个参数时 requestPermissions 方法传入的,第二个参数是申请的权限,Never null,第三个参数是是否授予权限的结果,也就是 PERMISSION_GRANTED 或者 PERMISSION_DENIED,Never null。

权限介绍

Android 的权限分成四个类别

  • normal 普通级别
  • dangerous 危险级别
  • signature 签名级别
  • signatureOrSystem 系统/签名级别

前两个权限直接定义时候即可,6.0以后 dangerous 可以在运行时申请。后两个权限为高级权限,拥有 platform 级别认证才可以申请,应用在没有权限情况下做受限操作,应用会被系统杀掉。

在 Manifest 中使用 来定义权限, 使用 来申请权限。申请的权限需要被系统或者其他应用定义,否则视为无效申请。

users Permission

申请普通权限可以使用 简单语法如下:

<uses-permission android:name="string"
        android:maxSdkVersion="integer" />

前一个参数简单,后以参数 maxSDKVersion 表示,如果应用从某一个版本开始不需要特定的权限,可以设置该属性。表示高于该 API Level 之后就不授予该权限。

如下定义

<uses-permission-sdk-23 android:name="string"
        android:maxSdkVersion="integer" />

表示只有当app运行在 API Level 23 或以上时才申请权限,以下不申请权限。

自定义权限

<permission android:description="string resource"
            android:icon="drawable resource"
            android:label="string resource"
            android:name="string"
            android:permissionGroup="string"
            android:protectionLevel=["normal" | "dangerous" |
                                     "signature" | "signatureOrSystem"] />
  • android:description:对权限的描述,比lable更加的详细,介绍该权限的相关使用情况,比如当用户被询问是否给其他应用该权限时。注意描述应该使用的是string资源,而不是直接使用string串。 android:icon:用来标识该权限的一个图标。
  • android:label:权限的一个给用户展示的简短描述。方便的来说,这个可以直接使用一个string字串来表示,但是如果要发布的话,还是应该使用string资源来表示。
  • android:name:权限的唯一名字,由于独立性,一般都是使用包名加权限名,该属性是必须的,其他的可选,未写的系统会指定默认值。
  • android:permissionGroup: 权限所属权限组的名称,并且需要在这个或其他应用中使用标签提前声明该名称,如果没有声明,该权限就不属于该组。
  • android:protectionLevel:权限的等级

reference


2016-09-27 Android , AndroidDev

Android Snackbar 使用

Snackbar 提供操作的轻量级反馈。显示在手机底部或者大屏幕的左下,Snackbar显示在所有界面的最上层,并且只显示一次。

Snackbar 可以包含一个操作,使用 setAction(CharSequence, android.view.View.onClickListener) 设置。 Snackbar 可以通过 setCallback(Callback) 来设置显示和消失的回调 Snackbar.Callback

显示时间长短的常量

  • int LENGTH_INDEFINITE 没有操作不消失
  • int LENGTH_LONG 显示长时间
  • int LENGTH_SHORT 显示短时间

Android Support Library (22.2.1) 起才支持 LENGTH_INDEFINITE。如果使用该属性, Snackbar 会一直显示,直到调用 dismiss() 或者下一个 Snackbar 出现。

make 方法的第一个参数表示 Snackbar 会寻找该 View 来hold Snackbar 的View。第二个参数为需要显示的字符串。第三个参数为显示时间,使用以上三个常量。

Snackbar.make(mLayout, R.string.permission_camera_rationale,
        Snackbar.LENGTH_INDEFINITE)
        .setAction(R.string.ok, new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{Manifest.permission.CAMERA},
                        REQUEST_CAMERA);
            }
        })
        .setCallback(new Snackbar.Callback() {
            @Override
            public void onDismissed(Snackbar snackbar, int event) {
                super.onDismissed(snackbar, event);
                Toast.makeText(getApplicationContext(), "onDismissed", Toast.LENGTH_LONG).show();
            }

            @Override
            public void onShown(Snackbar snackbar) {
                super.onShown(snackbar);
                Toast.makeText(getApplicationContext(), "onShown", Toast.LENGTH_LONG).show();
            }
        })
        .show();

综上,Snackbar 作为带响应的通知来说能带来不错的体验。相较于 Toast 来说,Snackbar 能够提供一种操作,对于修改内容来说,提供短时间内的撤销操作应该是不错的。其他能够想到的一些操作,比如撤销邮件的发送,撤销消息的发送,等等。


2016-09-26 Android , AndroidDev

电子书

最近文章

  • 每天学习一个命令:使用 rz sz 向服务器发送文件 搜索 rz sz 命令使用方式进来的,可以不用往下看了,直接学习 scp 或者 rsync 吧, rz sz 看了一下还是有很多限制的。
  • 分析家里局域网 WiFI 瓶颈 目前我的情况是,家中有一个千兆主路由放在客厅接外面的宽带,而我自己的房间有一台比较老的 Netgear 3800 路由来无线桥接连接外面的主路由,因为我不想我的 3800 路由中的设备暴露到主路由的设置中,所以用了 OpenWrt 的桥接模式。但是随着我在 3800 这台路下接的设备增多,导致目前 3800 这台路由不堪重负,已经影响到了我日常 streaming 局域网 NAS 中的电影。所以最近想更换一下这台已经 8 年历史的 WNDR 3800 路由器。
  • GitLab CI 使用笔记 CI/CD 不必多说。
  • 使用命令行远程网络唤起主机 在 Linux 下可以通过 etherwake 命令来网络唤醒设备。
  • Cloud-init 初始化虚拟机配置 在安装 Proxmox 后在它的文档中了解到了 cloud-init。所以就来梳理一下。