https://github.com/RikkaApps/SaveCopy #Android #code
build.gradle
app/src/main/java/app/rikka/savecopy
SaveActivity.java
太长不看,会有大致的梗概
build.gradle
apply plugin: "com.android.application"
android {
defaultConfig { compileSdkVersion 14
applicationId "app.rikka.savecopy"
versionCode 1; versionName "1.0"
minSdkVersion 14; targetSdkVersion 29
}
signingConfigs { sign }
buildTypes {
debug {
signingConfig signingConfigs.sign
}
release {
signingConfig signingConfigs.sign
minifyEnabled true; proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro')
shrinkResources true
}
}
buildFeatures { buildConfig = false }
lintOptions.checkReleaseBuilds = false
dependenciesInfo.includeInApk = false
}
android.applicationVariants.all { variant ->
variant.outputs.all { it.outputFileName = "save-copy-v${versionName}.apk" }
}
dependencies {
implementation "androidx.annotation:annotation:1.1.0"
}
apply from: rootProject.file("signing.gradle") app/src/main/java/app/rikka/savecopy
SaveActivity.java
import android.app.Activity;SaveService.java
import android.content.Intent;
import android.os.Bundle;
public class SaveActivity extends Activity {
@Override protected void onCreate(Bundle state) {
super.onCreate(state);
if (!getIntent().getAction() == Intent.ACTION_VIEW) return;
Intent passIntent = new Intent(getIntent());
passIntent.setClassName(this, SaveService.class.getName());
startService(passIntent);
finish();
}
}
太长不看,会有大致的梗概
GitHub
GitHub - RikkaApps/SaveCopy: Simple Android app, handle ACTION_VIEW, ACTION_SEND, ACTION_SEND_MULTIPLE, save a copy of the file.…
Simple Android app, handle ACTION_VIEW, ACTION_SEND, ACTION_SEND_MULTIPLE, save a copy of the file. For bad apps which only allow users to open files. - RikkaApps/SaveCopy
总的来说,代码太多挑重点看。先从 doSave 的实际保存逻辑开始。
当然还是不得不先从入口逻辑的 registry 看。
https://github.com/RikkaApps/SaveCopy/blob/435ddc702f79956e5d89aaf1050cafbd7e277df6/app/src/main/java/app/rikka/savecopy/SaveService.java#L149
总的来说是从 content solver 弄出带失败 fallback 的
啊,上面的函数不对,给个默认
注意下面的代码和上面的并行依赖于
上一步要产生
我先吐嘈吐嘈,以前王垠说要用
先来看看这个数据的 reference 在哪。
最后才是关键部分,用到
的时候并没有人觉得这该被包装成类似 Observer 的形式 😂。
其中
当然还是不得不先从入口逻辑的 registry 看。
public class SaveService extends IntentService {
public SaveService() { super("save-thread"); }
public SaveService(String name) { this(); }
protected void onHandleIntent(Intent intent) { onSave(intent); }
}
private void onSave(Intent intent) {
try { doSave(intent); }
catch (IOException | InterruptedException e) { e.printStackTrace(); }
}
private void doSave(Intent intent) throws IOException, InterruptedException https://github.com/RikkaApps/SaveCopy/blob/435ddc702f79956e5d89aaf1050cafbd7e277df6/app/src/main/java/app/rikka/savecopy/SaveService.java#L149
Uri data = intent.getData();
ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(data, null, null, null, null);
String displayName = "unknown-" + System.currentTimeMillis();
long totalSize = -1;
if (cursor != null && cursor.moveToFirst()) {
displayName = mapDefaultNon(-1, displayName, cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME), it -> it.getString(displayNameIndex));
totalSize = mapDefaultNon(-1, totalSize, cursor.getColumnIndex(OpenableColumns.SIZE), it -> it.getLong(sizeIndex));
cursor.close();
}
private <T> T takeNonOr(T value, T except, T default) {
return (value.equals(except))? default : value;
} //我也没有更好更接近 zero-cost 的方法了,用 Kotlin? 可是那个配置 proguard 更麻烦
//用工具方法?或者用 lambda? 总的来说是从 content solver 弄出带失败 fallback 的
String displayName; long totalSize; 啊,上面的函数不对,给个默认
mapDefaultNon 实现: (不得不说是很拗口,因为我拼写错了两次,但总的来看是值得的…… 虽然肯定不是唯一可行也绝对不该是最终方案)import java.util.function.Function;
private <T, R> R mapDefaultNon(T except, R default_value, T value, Function<T, R> transform) {
return (value.equals(except))? default_value : transform(value);
} 注意下面的代码和上面的并行依赖于
Uri data = intent.getData(); 引用。InputStream ins = cr.openInputStream(data); if (ins == null) return; //在 Kotlin 有等价 ?: return 形式来看看下一个生成 ContentValues 的逻辑
import MediaStore.MediaColumns //canonical type name?
ContentValues values = new ContentValues();
if (Build.VERSION.SDK_INT >= 29) {
values.put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
values.put(MediaColumns.IS_PENDING, true);
} else {
values.put(MediaColumns.DATA, Environment.getExternalStorageDirectory()+File.separator+Environment.DIRECTORY_DOWNLOADS+File.separator+displayName);
}
注,其实直接用 new File(dir, name) 而不是 File.seprator 拼接也很好啦values.put(MediaColumns.DISPLAY_NAME, displayName);
Uri target = EXTERNAL_CONTENT_URI;
}
其中private final Uri EXTERNAL_CONTENT_URI = (Build.VERSION.SDK_INT >= 29)? MediaStore.Downloads.EXTERNAL_CONTENT_URI : MediaStore.Files.getContentUri("external"); 上一步要产生
Uri target; ContentValues values; ,它们的数据依赖都是零散的,来看下一步Uri uri = cr.insert(target, values); if (uri == null) return; 我先吐嘈吐嘈,以前王垠说要用
Optional<T> 解决 null 问题,我真不觉得那会很好用,Kotlin 的设计多棒啊。int id = uri.toString().hashCode(); //顺便说一句,拿 hashCode() 不需要先 toString() 吧,既然是数据类的话先来看看这个数据的 reference 在哪。
scheduleNotification(id, builder); //仅被用于创建通知最后才是关键部分,用到
ParcelFileDescriptor , OutputStream 。ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(uri, "rw"); // 其实本来可以加个 takeOrThrow 完成Function内非局部返回的,但是想想还是算了 if (fd == null) return; OutputStream outs = new ParcelFileDescriptor.AutoCloseOutputStream(fd);接下来就是经典部分了,为了方便大家了解日常在你们手机里运行并且被重复无数遍的代码是什么样子,我直接粘贴好了(没
long writeSize = 0;也是蛮有意思的,其实只要是字节流操作,这样
byte[] buffer = new byte[8192];
for (int n; (n = ins.read(buffer)) != (-1);) {
outs.write(0, buffer, n);
outs.flush();
writeSize += n;
onBufferWrote.accept(writeSize);
}
outs.close(); ins.close();
Log.i("Save", writeSize, totalSize);
read(buffer); wirte(0, buffer, n) 的代码总会重复无数次,被我略略视为应该少用的 (-1) 也不得不使用,没有太好的解决方案因为通用的 android SDK 包创建的时候并没有人觉得这该被包装成类似 Observer 的形式 😂。
其中
onBufferWrote 在上面定义:import java.util.function.Consumer;1000 这个常量可以抽提,但我注意到
Consumer<Long> onBufferWrote = (n_wrote) -> {
if (totalSize == (-1) ) return;
int progress = (int) ((float)n_wrote/totalSize *100);
builder.setProgress(100, progress, true);
scheduleNotification(id, builder, 1000);
};
scheduleNotification(id, builder, ...) 有4处引用,包括省略参数的2处不能直接省略GitHub
RikkaApps/SaveCopy
Simple Android app, handle ACTION_VIEW, save a copy of the file. For bad apps which only allow users to open files. - RikkaApps/SaveCopy
duangsuse::Echo
总的来说,代码太多挑重点看。先从 doSave 的实际保存逻辑开始。 当然还是不得不先从入口逻辑的 registry 看。 public class SaveService extends IntentService { public SaveService() { super("save-thread"); } public SaveService(String name) { this(); } protected void onHandleIntent(Intent intent)…
最后就是更新不知道是什么的数据和发通知了(都是 service 里喽)
接下来我们看看 notifications 的实现吧
不过说实话,结合上下文我们应该知道这个重载可以这么写,只不过会落点微不足道的可配置 灵活性 (算了谁会那样调用啊,难不成这个也是可配置的?):
呃这附近讲的可能大概是 Handler, Message 的问题,也就是和 main loop 相关(迫真,但反正是和 infinite loop 有关了)
https://github.com/RikkaApps/SaveCopy/blob/master/app/src/main/java/app/rikka/savecopy/SaveService.java#L77-L119
这两个是比较贴近平台需求化的代码,不复制粘贴了。
不过我把里面的 new 改成了 make,理由也很明确,避免误会是写错了
标准 lifecycle callbacks, 只是不知道这里怎么有 conditions……
if (Build.SDK_INT >= 29) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.ImageColumns.IS_PENDING, false);
cr.update(uri, values, null, null);
}
🤔 [img]: false 是啥意思... 难不成对应 true 还有别的行为String text = Html.fromHTML(getString(R.string.note_saved_text),
String.format(HTML_SERIF, displayName),
String.format(HTML_SERIF, Environment.DIRECTORY_DOWNLOADS));
NotificationBuilder builder = makeNotificationBuilder(NOTIFICATION_CHANNEL_RESULT, getString(R.string.note_saved_title), text).setStyle(new Notification.BigTextStyle().bigText(text));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
builder.setPriority(Notification.PRIROITY_HIGH).setSound(Uri.EMPTY).setViberate(new long[0]);
scheduleNotification(id, builder); 接下来我们看看 notifications 的实现吧
private void scheduleNotifications(int id, Builder builder) { scheduleNotifications(id, builder, 0); }
private void scheduleNotifications(int id, Builder builder, long delay) {
if (delay == 0) {
mHandler.removeMessage(id);
mNotificationManager.notify(id, builder.build());
} else {
if (!mHandler.hasMessages(id)) mHandler.sendMessageDelayed(Message.obtain(mHandler, id, builder), delay);
}
} 不过说实话,结合上下文我们应该知道这个重载可以这么写,只不过会落点微不足道的
private void scheduleNotifications(int id, Builder builder) {
mHandler.removeMessage(id);
mNotificationManager.notify(id, builder.build());
} private void scheduleNotification(int id Builder builder) {
if (mHandler.hasMessages(id)) return;
mHandler.sendMessageDelayed(Message.obtain(mHandler, id, builder), delay);
}
呃这附近讲的可能大概是 Handler, Message 的问题,也就是和 main loop 相关(迫真,但反正是和 infinite loop 有关了)
https://github.com/RikkaApps/SaveCopy/blob/master/app/src/main/java/app/rikka/savecopy/SaveService.java#L77-L119
public Notification.Builder newNotificationBuilder(String channelId, CharSequence title, CharSequence text)
@TargetApi(Build.VERSION_CODES.O) public void onCreateNotificationChannel(@NonNull NotificationManager nm) 这两个是比较贴近平台需求化的代码,不复制粘贴了。
不过我把里面的 new 改成了 make,理由也很明确,避免误会是写错了
new Wtf() 成 newWtf() 啊private NotificationManager mNotificationManager;//private Map<Integer, Notification> m;
private Handler mHandler;
@Override public void onCreate() {
super.onCreate();
mHandler = new Handler(Looper.getMainLooper(), msg -> {
if (msg.obj instanceof Notification.Builder) {
mNotificationManager.notify(msg.what, ((Notification.Builder) msg.obj).build());
return true; //^ 这个重复逻辑没法抽提,好难受啊
}
return false; //< 这种不明确的 bool “枚举”是 Java C# 这类语言的通病,要解决对语言模块化和明确性影响很大
});
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//noinspection ConstantConditions
onCreateNotificationChannel(mNotificationManager);
startForeground(NOTIFICATION_ID_PROGRESS, onStartForeground());
}
看了半天才注意到 Message { int what; Object obj } 被用到了,我开始还以为 msg 已经是 Notification.Builder 没想到是 msg.obj 判取共引用了两次……话说回来 Java 14 学着Kotlin支持 if (x instanceof Notification.Builder builder) 这种了 😂标准 lifecycle callbacks, 只是不知道这里怎么有 conditions……
//noinspection ConstantConditions话说回来,其实这一段里
onCreateNotificationChannel(mNotificationManager);
mNotificationManager 也可以不要 mXXX ,变成一个局部变量 (lambda capture),不过介于下面 scheduleNotification 要“低开销”即时发送也就算了NotificationManager notificationManager = makeNotificationManager();下面这个逻辑说来也奇怪,它不是 event handler 也和
mHandler = new Handler(Looper.getMainLooper(), msg -> {/**/});
onXXX 无关却起了 onXXX 的名字…… 理论上 onXXX() 都该是 void 的吧?public Notification onStartForeground() {
return makeNotificationBuilder(NOTIFICATION_CHANNEL_PROGRESS, getString(R.string.notification_working_title), null).build(); // 想了半天才记起来第三个 null 是 notification content……
}GitHub
RikkaApps/SaveCopy
Simple Android app, handle ACTION_VIEW, save a copy of the file. For bad apps which only allow users to open files. - RikkaApps/SaveCopy
duangsuse::Echo
最后就是更新不知道是什么的数据和发通知了(都是 service 里喽) if (Build.SDK_INT >= 29) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.ImageColumns.IS_PENDING, false); cr.update(uri, values, null, null); } 🤔 [img]: false 是啥意思... 难不成对应 true 还有别的行为 …
This media is not supported in your browser
VIEW IN TELEGRAM
duangsuse::Echo
最后就是更新不知道是什么的数据和发通知了(都是 service 里喽) if (Build.SDK_INT >= 29) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.ImageColumns.IS_PENDING, false); cr.update(uri, values, null, null); } 🤔 [img]: false 是啥意思... 难不成对应 true 还有别的行为 …
讲完了。总结一下梗概:虽然是一个小应用 (onHandleIntent, onSave, doSave),但 notification 部分还是很多代码的……
如果可能的话,我觉得这一部分应该抽象成
如果可能的话,我觉得这一部分应该抽象成
ScheduleNotificationIntentService 毕竟 onCreate 里几乎没有无关代码Intent, Bundle, Uri; Message, Handler, Looper; app.Notification, text.Html
ContentSolver, ContentValues, database.Cursor
os.Build; os.Environment.DIR_DOWNLOADS
content.ParcelFileDescriptor
provider.MediaStore , provider.OpenableColumns
File, InputStream, OutputStream, IOException
List, Map/*unused*/, ArrayList#task 我来总结一下被鸽子的小项目:
LiterateKt (怎么连
Bank Editor (稍后会有重写之前的 Qt 周期执行小应用,就是
(稍后也会想复制那个 sin wave 用到 QAudioDevice 的小程序)
(稍后还会有一个简单的 GTK# TreeView 示例 )
LiterateKt (怎么连
class Project(File, Config) 的子项目树模型都总结不出来…… 还总是在 Markdown 语法上纠缠,用 java.util.regex.Pattern 不就可以了,项目都可 # Include,root项目可以有 # LiterateKt )Bank Editor (稍后会有重写之前的 Qt 周期执行小应用,就是
QSettings 和 QTimer ,顺便包括 rectangle(0,0,p*width,height) 的自定义绘制,signal/slot 模型的基础使用嘛…… 不懂得怎么抽提出子程序)(稍后也会想复制那个 sin wave 用到 QAudioDevice 的小程序)
(稍后还会有一个简单的 GTK# TreeView 示例 )
audio_output.pro
那么整个类余下的定义
还有 overrides:
TARGET = audio_output那么,架构器和预逻辑
QT += widgets multimedia
SOURCES += main.cpp audio_output.coo
HEADERS += primitive_data.hpp helper.hpp audio_output.hpp
target.path = /bin/
INSTALLS += target
AudioGenerator::AudioGenerator(QAudioFormat format, usec durationUs, cnt sampleRate): format(format), durationUs(durationUs), sampleRate(sampleRate) {
require(format.isValid(), "invalid format");
generateData();
}
void AudioGenerator::start() { open(QIODevice::ReadOnly); }
void AudioGenerator::stop() { position = 0; close(); }
然后我们看看生成波形数据的。void AudioGenerator::generateData()
{
int bytePerSample = format.sampleSize() / BYTE_BITS;
int bytePerSec = format.sampleRate() * bytePerSample;
// [sample]: (rate * size) * n_channel
cnt n_byte = format.channelCount() * (this->durationUs / SEC_US * bytePerSec);
Q_ASSERT(n_byte % bytePerSec == 0);
audioBuffer.resize(n_byte);
byte* ys = reinterpret_cast<byte*>(audioBuffer.data());
for (idx i=0, x=0; i != n_byte; i += bytePerSample, x += 1) {
const qreal y = waveY(x);
for (int _t=0; _t<format.channelCount(); _t++) { writeSample(ys, i, y); }
}
}
qreal AudioGenerator::waveY(unsigned x) {
return qCos(2 * M_PI * sampleRate * qreal(x % format.sampleRate()) / format.sampleRate());
} 那么整个类余下的定义
class AudioGenerator: public QIODevice
{ Q_OBJECT
private:
idx position = 0;
QByteArray audioBuffer;
void generateData();
qreal waveY(int x);
void writeUInt16(byte* dst, quint16 v);
void writeSample(byte* ys, idx i, qreal y);
private:
QAudioFormat format;
long durationUs;
int sampleRate; 还有 overrides:
public:
longcnt readData(byte* dst, longcnt max_len) override;
longcnt writeData(const byte* src, longcnt len) override;
longcnt bytesAvailable() const override;它们(readData, writeData, bytesAvailable)的实现则是
longcnt AudioGenerator::readData(char* data, longcnt len)
{
if (audioBuffer.isEmpty()) return 0;
cnt pos = 0;
for (cnt nChunk; pos < len; pos += nChunk) {
cnt nBufferRest = audioBuffer.size() - position;
nChunk = qMin(cnt(len) - pos, nBufferRest);
memcpy(data+pos, audioBuffer.constData()+position, nChunk);
position = coerceCycle(position + nChunk, cnt(audioBuffer.size()));
}
return pos; //count read
}
下面两个不重要, write 直接返回; bytesAvailable 直接 return super() + audioBuffer.lengthqint64 AudioGenerator::writeData(const char *data, qint64 len)
{
Q_UNUSED(data); Q_UNUSED(len);
return 0;
}
qint64 AudioGenerator::bytesAvailable() const
{
return audioBuffer.size() + QIODevice::bytesAvailable();
}auto AudioGenerator::writeUInt16(byte* dst, quint16 v) -> void {
if (this->format.byteOrder() == QAudioFormat::LittleEndian) qToLittleEndian(v, dst);
else qToBigEndian(v, dst);
};
auto AudioGenerator::writeSample(byte* ys, idx i, qreal y) -> void {
auto sampleType = format.sampleType();
switch (format.sampleSize()) {
case BYTE_BITS:
if (sampleType == QAudioFormat::UnSignedInt) {
ys[i] = static_cast<u8>((1.0 + y) / 2 * QUINT8_MAX);
} else if (format.sampleType() == QAudioFormat::SignedInt) {
ys[i] = static_cast<i8>(y * QINT8_MAX);
} else unsupported();
break;
case SHORT_BITS:
if (sampleType == QAudioFormat::UnSignedInt) {
writeUInt16(&ys[i], static_cast<u16>((1.0 + y) / 2 * QUINT16_MAX));
} else if (format.sampleType() == QAudioFormat::SignedInt) {
writeUInt16(&ys[i], static_cast<i16>(y * QINT16_MAX));
} else unsupported();
break;
default:
unsupported();
break;
}
}
最后发完这两个,就算是说完了……欸 其实我什么也没说,这个有啥意思…… 不该发void writeUInt16(byte*dst, i16 v);
void writeSample(byte* ys, idx i, qreal y); 说实话这根本不应该放在一个应用类里吧……
#Qt 🤔 啊,我才知道 QTimer 默认就是 scheduleAtFixedRate ,而不需要为非 singleShot 的情况额外编程的,只需 timer->start(rate); timer->remainingTime() 就好了,真方便
template <typename T>PREETY_FUNCTION — StackOverflow
void prints(T obj) { cout << obj << endl; }
template <typename... Arg>
void prints(T obj, Arg... args) { cout << obj; prints(args); }
void prints(std::initializer_list texts) {
for (auto text : texts) cout << text;
cout << endl;
}
Stack Overflow
Variable number of arguments in C++?
How can I write a function that accepts a variable number of arguments? Is this possible, how?
audio_output
68.9 KB
duangsuse::Echo
#task 我来总结一下被鸽子的小项目: LiterateKt (怎么连 class Project(File, Config) 的子项目树模型都总结不出来…… 还总是在 Markdown 语法上纠缠,用 java.util.regex.Pattern 不就可以了,项目都可 # Include,root项目可以有 # LiterateKt ) Bank Editor (稍后会有重写之前的 Qt 周期执行小应用,就是 QSettings 和 QTimer ,顺便包括 rectangle(0,0,p*width…
This media is not supported in your browser
VIEW IN TELEGRAM
不写了不写了…… 不就是个 QSettings 和 QTimer 还有 Gtk# 的 TreeView 使用么…… 模型都是简单的,再复杂不过一个 table grid 而已
来看看别的
嗯……然后我们想弄一个能组合歌词的程序,比如这里有一个示例数据
等我弄完
来看看别的
from re import compile
PAT_LRC_ENTRY = compile(r"[\[<](\d{2}):(\d{2}).(\d{2})[>\]] ?([^<\n]*)")
def readLrc(text):
def readEntry(g): return (int(g[0])*60 + int(g[1]) + int(g[2]) / 100, g[3]) # [mm:ss.xx] content
return [readEntry(e) for e in PAT_LRC_ENTRY.findall(text)] def dumpLrc(lrc_lines):
def header(t, surr="[]"): return "%s%02i:%02i.%02i%s" %(surr[0], t/60, t%60, t%1.0 * 100, surr[1])
return "\n".join([header(lrcs[0][0]) + lrcs[0][1] + " ".join([header(t, "<>") + s for (t, s) in lrcs[1:]]) for lrcs in lrc_lines])
嗯……然后我们想弄一个能组合歌词的程序,比如这里有一个示例数据
[00:32.96]怎<00:33.12>么<00:33.36>大<00:33.76>风<00:34.16>越<00:34.56>狠<00:36.16>我<00:36.56>心<00:36.96>越<00:37.36>荡<00:39.36>幻<00:39.52>如<00:39.68>一<00:40.08>丝<00:40.32>尘<00:40.72>土<00:41.12>随<00:41.28>风<00:41.52>自<00:41.92>由<00:42.16>的<00:42.56>在<00:43.76>狂<00:44.16>舞<00:45.76>我<00:45.92>要<00:46.08>握<00:46.32>紧<00:46.72>手<00:47.12>中<00:47.36>坚<00:47.52>定<00:48.96>却<00:49.12>又<00:49.36>飘<00:49.76>散<00:50.16>的<00:50.56>勇<00:50.96>气<00:52.24>我<00:52.40>会<00:52.56>变<00:52.96>成<00:53.36>巨<00:53.52>人<00:54.16>踏<00:54.40>着<00:54.56>力<00:54.72>气<00:54.96>踩<00:55.36>着<00:55.76>梦然后我们有楼上的 zipWithNext 和 zipTakeWhile 实现
等我弄完
Telegram
duangsuse::Echo
import operator
from functools import partial
吐嘈:functools 里面居然没有 compose 也不支持 partial(print, ..., 1),虽然后来我发现即便 lambda 这个很长但也可以用,emmm
def require(value, p, msg):
if not p(value): raise ValueError(f"{msg}: {value}")
def zipWithNext(xs):
require(xs, lambda…
from functools import partial
吐嘈:functools 里面居然没有 compose 也不支持 partial(print, ..., 1),虽然后来我发现即便 lambda 这个很长但也可以用,emmm
def require(value, p, msg):
if not p(value): raise ValueError(f"{msg}: {value}")
def zipWithNext(xs):
require(xs, lambda…