diff --git a/.github/workflows/preview-apk.yml b/.github/workflows/preview-apk.yml index 7d72253b6..e77f9aa99 100644 --- a/.github/workflows/preview-apk.yml +++ b/.github/workflows/preview-apk.yml @@ -42,8 +42,8 @@ jobs: env: ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} run: | - export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/:${ANDROID_NDK_ROOT}" - ./scripts/install-toolchains.sh && ./ndk-make.sh + export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/" + scripts/install-toolchains.sh && scripts/ndk-make.sh - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b0d152dcf..f4e4ffeb8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -30,6 +30,7 @@ + @@ -288,7 +289,8 @@ + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" + android:exported="true"> + + + + diff --git a/res/menu/webxdc.xml b/res/menu/webxdc.xml index d119a5c72..20bd61f0c 100644 --- a/res/menu/webxdc.xml +++ b/res/menu/webxdc.xml @@ -2,6 +2,8 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index c83cacf86..c2c425804 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -268,6 +268,8 @@ Original message not found Reply Privately Source Code + + Add to Home Screen Mute for 1 hour Mute for 2 hours diff --git a/scripts/ndk-make.sh b/scripts/ndk-make.sh new file mode 100755 index 000000000..eb03de147 --- /dev/null +++ b/scripts/ndk-make.sh @@ -0,0 +1,159 @@ +#!/bin/sh + +# If you want to speed up compilation, you can run this script with your +# architecture as an argument: +# +# scripts/ndk-make.sh arm64-v8a +# +# Possible values are armeabi-v7a, arm64-v8a, x86 and x86_64. +# You should be able to find out your architecture by running: +# +# adb shell uname -m +# +# Or you just guess your phone's architecture to be arm64-v8a and if you +# guessed wrongly, a warning message will pop up inside the app, telling +# you what the correct one is. +# +# The values in the following lines mean the same: +# +# armeabi-v7a, armv7 and arm +# arm64-v8a, aarch64 and arm64 +# x86 and i686 +# (there are no synonyms for x86_64) +# +# +# If you put this in your .bashrc, then you can directly build and +# deploy DeltaChat from the jni/deltachat-core-rust directory by +# typing `nmake`: +# +# nmake() {(cd ../..; scripts/ndk-make.sh arm64-v8a && ./gradlew installFatDebug; notify-send "install finished")} +# +# +# If anything doesn't work, please open an issue!! + +set -e +echo "starting time: `date`" + +: "${ANDROID_NDK_ROOT:=$ANDROID_NDK_HOME}" +: "${ANDROID_NDK_ROOT:=$ANDROID_NDK}" +if test -z "$ANDROID_NDK_ROOT"; then + echo "ANDROID_NDK_ROOT is not set" + exit 1 +fi + +echo Setting CARGO_TARGET environment variables. + +if test -z "$NDK_HOST_TAG"; then + KERNEL="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + NDK_HOST_TAG="$KERNEL-$ARCH" +fi + +TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$NDK_HOST_TAG" +export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$TOOLCHAIN/bin/armv7a-linux-androideabi16-clang" +export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android21-clang" +export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/i686-linux-android16-clang" +export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/x86_64-linux-android21-clang" + +# Check if the argument is a correct architecture: +if test $1 && echo "armeabi-v7a arm64-v8a x86 x86_64" | grep -vwq $1; then + echo "Architecture '$1' not known, possible values are armeabi-v7a, arm64-v8a, x86 and x86_64." + exit +fi + +cd jni +jnidir=$PWD +rm -f armeabi-v7a/* +rm -f arm64-v8a/* +rm -f x86/* +rm -f x86_64/* +mkdir -p armeabi-v7a +mkdir -p arm64-v8a +mkdir -p x86 +mkdir -p x86_64 + +cd deltachat-core-rust + +# fix build on MacOS Catalina +unset CPATH + +if test -z $1; then + echo Full build + + # According to 1.45.0 changelog in https://github.com/rust-lang/rust/blob/master/RELEASES.md, + # "The recommended way to control LTO is with Cargo profiles, either in Cargo.toml or .cargo/config, or by setting CARGO_PROFILE__LTO in the environment." + export CARGO_PROFILE_RELEASE_LTO=on + RELEASE="release" + RELEASEFLAG="--release" +else + echo Fast, partial, slow debug build. DO NOT UPLOAD THE APK ANYWHERE. + + RELEASE="debug" + RELEASEFLAG= +fi + +# Work around the bug in the build of Rust standard library. +# It is built against r22b NDK toolchains and still requires -lgcc instead of -lunwind used in newer r23 NDK. +# See discussion at +TMPLIB="$(mktemp -d)" +if test -z "$(find "$ANDROID_NDK_ROOT" -name libgcc.a -print -quit)"; then + # NDK is does not contain libgcc.a, add a linker script to fake it. + echo 'INPUT(-lunwind)' >"$TMPLIB/libgcc.a" +fi + +if test -z $1 || test $1 = armeabi-v7a; then + echo "-- cross compiling to armv7-linux-androideabi (arm) --" + export CFLAGS=-D__ANDROID_API__=16 + TARGET_CC=armv7a-linux-androideabi16-clang \ + TARGET_AR=llvm-ar \ + cargo +`cat rust-toolchain` rustc $RELEASEFLAG --target armv7-linux-androideabi -p deltachat_ffi -- -L "$TMPLIB" + cp target/armv7-linux-androideabi/$RELEASE/libdeltachat.a $jnidir/armeabi-v7a +fi + +if test -z $1 || test $1 = arm64-v8a; then + echo "-- cross compiling to aarch64-linux-android (arm64) --" + export CFLAGS=-D__ANDROID_API__=21 + TARGET_CC=aarch64-linux-android21-clang \ + TARGET_AR=llvm-ar \ + cargo +`cat rust-toolchain` rustc $RELEASEFLAG --target aarch64-linux-android -p deltachat_ffi -- -L "$TMPLIB" + cp target/aarch64-linux-android/$RELEASE/libdeltachat.a $jnidir/arm64-v8a +fi + +if test -z $1 || test $1 = x86; then + echo "-- cross compiling to i686-linux-android (x86) --" + export CFLAGS=-D__ANDROID_API__=16 + TARGET_CC=i686-linux-android16-clang \ + TARGET_AR=llvm-ar \ + cargo +`cat rust-toolchain` rustc $RELEASEFLAG --target i686-linux-android -p deltachat_ffi -- -L "$TMPLIB" + cp target/i686-linux-android/$RELEASE/libdeltachat.a $jnidir/x86 +fi + +if test -z $1 || test $1 = x86_64; then + echo "-- cross compiling to x86_64-linux-android (x86_64) --" + export CFLAGS=-D__ANDROID_API__=21 + TARGET_CC=x86_64-linux-android21-clang \ + TARGET_AR=llvm-ar \ + cargo +`cat rust-toolchain` rustc $RELEASEFLAG --target x86_64-linux-android -p deltachat_ffi -- -L "$TMPLIB" + cp target/x86_64-linux-android/$RELEASE/libdeltachat.a $jnidir/x86_64 +fi + +rm -fr "$TMPLIB" + +echo -- ndk-build -- + +cd ../.. + +if test $1; then + "$ANDROID_NDK_ROOT/ndk-build" APP_ABI="$1" +else + # We are compiling for all architectures defined in Application.mk + "$ANDROID_NDK_ROOT/ndk-build" +fi + +if test $1; then + echo "NDK_ARCH=$1" >ndkArch +else + rm -f ndkArch # Remove ndkArch, ignore if it doesn't exist +fi + +echo "ending time: `date`" diff --git a/src/com/b44t/messenger/DcMsg.java b/src/com/b44t/messenger/DcMsg.java index cd53e65b7..8be2823e1 100644 --- a/src/com/b44t/messenger/DcMsg.java +++ b/src/com/b44t/messenger/DcMsg.java @@ -52,6 +52,10 @@ public class DcMsg { this.msgCPtr = msgCPtr; } + public boolean isOk() { + return msgCPtr != 0; + } + @Override protected void finalize() throws Throwable { super.finalize(); diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index b3e97d2d4..1ab00f718 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -322,6 +322,7 @@ public class ConversationFragment extends MessageSelectorFragment menu.findItem(R.id.menu_context_reply_privately).setVisible(false); menu.findItem(R.id.menu_context_remove_contact).setVisible(false); menu.findItem(R.id.menu_context_add_contact).setVisible(false); + menu.findItem(R.id.menu_add_to_home_screen).setVisible(false); } else { DcMsg messageRecord = messageRecords.iterator().next(); DcChat chat = getListAdapter().getChat(); @@ -345,6 +346,7 @@ public class ConversationFragment extends MessageSelectorFragment } menu.findItem(R.id.menu_context_remove_contact).setVisible(isMember); menu.findItem(R.id.menu_context_add_contact).setVisible(canModify && !isMember); + menu.findItem(R.id.menu_add_to_home_screen).setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC); } // if one of the selected items cannot be saved, disable saving. @@ -961,6 +963,10 @@ public class ConversationFragment extends MessageSelectorFragment handleForwardMessage(getListAdapter().getSelectedItems()); actionMode.finish(); return true; + case R.id.menu_add_to_home_screen: + WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId()); + actionMode.finish(); + return true; case R.id.menu_context_save_attachment: handleSaveAttachment(getListAdapter().getSelectedItems()); return true; diff --git a/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java b/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java index bfcdea7a3..e0a45f078 100644 --- a/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java +++ b/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java @@ -182,6 +182,9 @@ public class ProfileDocumentsFragment menu.findItem(R.id.details).setVisible(singleSelection); menu.findItem(R.id.show_in_chat).setVisible(singleSelection); menu.findItem(R.id.share).setVisible(singleSelection); + + boolean webxdcApp = singleSelection && messageRecords.iterator().next().getType() == DcMsg.DC_MSG_WEBXDC; + menu.findItem(R.id.menu_add_to_home_screen).setVisible(webxdcApp); } private ProfileDocumentsAdapter getListAdapter() { @@ -225,6 +228,10 @@ public class ProfileDocumentsFragment case R.id.share: handleShare(getSelectedMessageRecord(getListAdapter().getSelectedMedia())); return true; + case R.id.menu_add_to_home_screen: + WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedMedia()).getId()); + mode.finish(); + return true; case R.id.show_in_chat: handleShowInChat(getSelectedMessageRecord(getListAdapter().getSelectedMedia())); return true; diff --git a/src/org/thoughtcrime/securesms/WebxdcActivity.java b/src/org/thoughtcrime/securesms/WebxdcActivity.java index 10d5063da..618ee23c1 100644 --- a/src/org/thoughtcrime/securesms/WebxdcActivity.java +++ b/src/org/thoughtcrime/securesms/WebxdcActivity.java @@ -1,7 +1,11 @@ package org.thoughtcrime.securesms; +import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -17,6 +21,10 @@ import android.webkit.WebView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.core.app.TaskStackBuilder; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; @@ -41,6 +49,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE private String baseURL; private String sourceCodeUrl = ""; + + public static void openWebxdcActivity(Context context, DcMsg instance) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Prefs.isDeveloperModeEnabled(context)) { @@ -50,9 +60,31 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE Toast.makeText(context, "WARNING! At least Android 5.0 (Lollipop) is needed for Webxdc to be safe! Use at your own risk!", Toast.LENGTH_LONG).show(); } - Intent intent =new Intent(context, WebxdcActivity.class); - intent.putExtra("appMessageId", instance.getId()); - context.startActivity(intent); + context.startActivity(getWebxdcIntent(context, instance.getId())); + } + + private static Intent getWebxdcIntent(Context context, int msgId) { + DcContext dcContext = DcHelper.getContext(context); + Intent intent = new Intent(context, WebxdcActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra("accountId", dcContext.getAccountId()); + intent.putExtra("appMessageId", msgId); + return intent; + } + + private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) { + DcContext dcContext = DcHelper.getContext(context); + + final Intent chatIntent = new Intent(context, ConversationActivity.class) + .putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId()) + .setAction(Intent.ACTION_VIEW); + + final Intent webxdcIntent = getWebxdcIntent(context, msgId); + + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(chatIntent) + .addNextIntent(webxdcIntent) + .getIntents(); } @Override @@ -65,7 +97,19 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE int appMessageId = b.getInt("appMessageId"); this.dcContext = DcHelper.getContext(getApplicationContext()); + if (dcContext.getAccountId() != b.getInt("accountId")) { + Toast.makeText(this, "Switch to belonging account first.", Toast.LENGTH_LONG).show(); + finish(); + return; + } + this.dcAppMsg = this.dcContext.getMsg(appMessageId); + if (!this.dcAppMsg.isOk()) { + Toast.makeText(this, "Webxdc does no longer exist.", Toast.LENGTH_LONG).show(); + finish(); + return; + } + // `msg_id` in the subdomain makes sure, different apps using same files do not share the same cache entry // (WebView may use a global cache shared across objects). // (a random-id would also work, but would need maintenance and does not add benefits as we regard the file-part interceptRequest() only, @@ -107,6 +151,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { + case R.id.menu_add_to_home_screen: + addToHomeScreen(this, dcAppMsg.getId()); + return true; case R.id.source_code: openUrlInBrowser(this, sourceCodeUrl); return true; @@ -192,6 +239,36 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE }); } + public static void addToHomeScreen(Activity activity, int msgId) { + Context context = activity.getApplicationContext(); + try { + DcContext dcContext = DcHelper.getContext(context); + DcMsg msg = dcContext.getMsg(msgId); + final JSONObject info = msg.getWebxdcInfo(); + + final String docName = JsonUtils.optString(info, "document"); + final String xdcName = JsonUtils.optString(info, "name"); + byte[] blob = msg.getWebxdcBlob(JsonUtils.optString(info, "icon")); + ByteArrayInputStream is = new ByteArrayInputStream(blob); + BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon"); + Bitmap bitmap = drawable.getBitmap(); + + ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId) + .setShortLabel(docName.isEmpty() ? xdcName : docName) + .setIcon(IconCompat.createWithBitmap(bitmap)) + .setIntents(getWebxdcIntentWithParentStack(context, msgId)) + .build(); + + if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) { + Toast.makeText(context, R.string.done, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show(); + } + } catch(Exception e) { + Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show(); + } + } + class InternalJSApi { @JavascriptInterface public String selfAddr() { diff --git a/src/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java b/src/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java index 8bb6c248b..3245ec612 100644 --- a/src/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java +++ b/src/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java @@ -28,6 +28,7 @@ public class ConversationAdaptiveActionsToolbar extends Toolbar { private static final int ID_NEVER_SHOW_AS_ACTION_3 = R.id.menu_context_remove_contact; private static final int ID_NEVER_SHOW_AS_ACTION_4 = R.id.menu_context_add_contact; private static final int ID_NEVER_SHOW_AS_ACTION_5 = R.id.menu_resend; + private static final int ID_NEVER_SHOW_AS_ACTION_6 = R.id.menu_add_to_home_screen; private static final int ID_ALWAYS_SHOW_AS_ACTION = R.id.menu_context_forward; private int maxShown; @@ -85,7 +86,8 @@ public class ConversationAdaptiveActionsToolbar extends Toolbar { || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_2 || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_3 || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_4 - || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_5; + || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_5 + || item.getItemId() == ID_NEVER_SHOW_AS_ACTION_6; boolean alwaysShowAsAction = item.getItemId() == ID_ALWAYS_SHOW_AS_ACTION; if (alwaysShowAsAction) continue;