diff --git a/CHANGES.rst b/CHANGES.rst index cede3344bc..0524c43df0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,39 @@ +Changes in Riot 0.8.1 (2018-02-15) +=================================================== + +Improvements: + * Update matrix-sdk.aar lib (v0.9.0). + +Bug Fix: + * URL Preview: We should have it for m.notice too (PR 1975). + +Changes in Riot 0.8.00-beta (2018-02-02) +=================================================== + +Features: + + * Add a new tab to list the user's communities (vector-im/riot-meta/#114). + * Add new screens to display the community details, edition is not supported yet (vector-im/riot-meta/#115, vector-im/riot-meta/#116, vector-im/riot-meta/#117). + * Room Settings: handle the related communities in order to show flair for them. + * User Settings: Let the user enable his community flair in rooms configured to show it. + * Add the url preview feature (PR #1929). + +Improvements: + + * Support the 4 states for the room notification level (all messages (noisy), all messages, mention only, mute). + * Add the avatar to the pills displayed in room history (PR #1917). + * Set the push server URLs as a resource string (PR #1908). + * Improve duplicate events detection (#1907). + * Vibrate when long pressing on an user name / avatar to copy his/her name in the edit text. + * Improve the notifications management. + +Bugfixes: + + * #1903: Weird room layout. + * #1896: Copy source code of a message. + * #1821, #1850: Improve the text sharing. + * #1920: Phone vibrates when mentioning someone. + Changes in Riot 0.7.09 (2018-01-16) =================================================== diff --git a/README.md b/README.md index d3e79fc8e1..e74739b31e 100755 --- a/README.md +++ b/README.md @@ -95,6 +95,21 @@ Customise your flavour You will need to manage your own provider because "im.vector" is already used (look at VectorContentProvider to manage it). +Customise your application settings with a custom google play link +=================================================================== + +It is possible to set some default values to Riot with some extra parameters to the google play link. + +- Use the https://developers.google.com/analytics/devguides/collection/android/v4/campaigns URL generator (at the bottom) + +Set "Campaign Source" +Set "Campaign Content" with the extra parameters (e.g. is=http://my__is.org&hs=http://my_hs.org) +Generate the customised link + +- Supported extra parameters +is : identidy server URL +hs : home server URL + FAQ === diff --git a/build.gradle b/build.gradle index 76d6c77598..aedecf69a6 100755 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,8 @@ buildscript { // global properties used in sub modules ext { - versionCodeProp = 70900 - versionNameProp = "0.7.09" + versionCodeProp = 80001 + versionNameProp = "0.8.1" versionBuild = System.getenv("BUILD_NUMBER") as Integer ?: 0 buildNumberProp = "${versionBuild}" } diff --git a/set_debug_env.sh b/set_debug_env.sh new file mode 100644 index 0000000000..9f0bc7690e --- /dev/null +++ b/set_debug_env.sh @@ -0,0 +1,17 @@ +echo remove the SDK lib +rm -f vector/libs/matrix-sdk.aar +rm -rf vector/build + +echo remove sdk folder +rm -rf ../matrix-android-sdk + +echo clone the git folder +git clone -b develop https://github.com/matrix-org/matrix-android-sdk ../matrix-android-sdk + +echo replace step 1 +sed -i '' -e 's/\/\/include/include/' settings.gradle || true +sed -i '' -e 's/\/\/project/project/' settings.gradle || true + +echo replace step 2 +sed -i '' -e "s/compile(name: 'matrix/\/\/compile(name: 'matrix/" vector/build.gradle || true +sed -i '' -e "s/\/\/compile project(':matrix-/compile project(':matrix-/" vector/build.gradle || true diff --git a/vector/build.gradle b/vector/build.gradle index 6db30bcfcf..174a98944b 100755 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -102,6 +102,7 @@ android { disable 'InvalidPackage' disable 'MissingTranslation' disable 'RestrictedApi' + disable 'ImpliedQuantity' } repositories { @@ -189,6 +190,7 @@ dependencies { compile(name: 'react-native', ext: 'aar') compile(name: 'react-native-locale-detector', ext: 'aar') + // another tracking than GA compile 'org.piwik.sdk:piwik-sdk:2.0.0' diff --git a/vector/libs/matrix-sdk.aar b/vector/libs/matrix-sdk.aar index a9f3eec62d..7a334f055b 100644 Binary files a/vector/libs/matrix-sdk.aar and b/vector/libs/matrix-sdk.aar differ diff --git a/vector/src/app/java/im/vector/gcm/MatrixGcmListenerService.java b/vector/src/app/java/im/vector/gcm/MatrixGcmListenerService.java index 1cccb2d9c0..58501220dd 100755 --- a/vector/src/app/java/im/vector/gcm/MatrixGcmListenerService.java +++ b/vector/src/app/java/im/vector/gcm/MatrixGcmListenerService.java @@ -33,7 +33,6 @@ import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.services.EventStreamService; -import im.vector.util.PreferencesManager; /** * Class implementing GcmListenerService. @@ -65,7 +64,10 @@ private Event parseEvent(Map data) { event.sender = data.get("sender"); event.roomId = data.get("room_id"); event.setType(data.get("type")); - event.updateContent((new JsonParser()).parse(data.get("content")).getAsJsonObject()); + + if (data.containsKey("content")) { + event.updateContent((new JsonParser()).parse(data.get("content")).getAsJsonObject()); + } return event; } catch (Exception e) { @@ -112,27 +114,13 @@ private void onMessageReceivedInternal(final Map data) { Log.d(LOG_TAG, "## onMessageReceivedInternal() : the notifications are disabled"); return; } - - boolean useBatteryOptim = !PreferencesManager.canStartBackgroundService(getApplicationContext()) && EventStreamService.isStopped(); - - if ((!gcmManager.isBackgroundSyncAllowed() || useBatteryOptim) - && VectorApp.isAppInBackground()) { - + if (!gcmManager.isBackgroundSyncAllowed() && VectorApp.isAppInBackground()) { EventStreamService eventStreamService = EventStreamService.getInstance(); Event event = parseEvent(data); - if (!gcmManager.isBackgroundSyncAllowed()) { - Log.d(LOG_TAG, "## onMessageReceivedInternal() : the background sync is disabled with eventStreamService " + eventStreamService); - } else { - Log.d(LOG_TAG, "## onMessageReceivedInternal() : use the battery optimisation with eventStreamService " + eventStreamService); - } - - if (null != eventStreamService) { - eventStreamService.onNotifiedEventWithBackgroundSyncDisabled(event, data.get("room_name"), data.get("sender_display_name"), unreadCount); - } else { - EventStreamService.onStaticNotifiedEvent(getApplicationContext(), event, data.get("room_name"), data.get("sender_display_name"), unreadCount); - } + Log.d(LOG_TAG, "## onMessageReceivedInternal() : the background sync is disabled with eventStreamService " + eventStreamService); + EventStreamService.onStaticNotifiedEvent(getApplicationContext(), event, data.get("room_name"), data.get("sender_display_name"), unreadCount); return; } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 76d8f965b0..c900e5082b 100755 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -318,6 +318,15 @@ android:label="@string/title_activity_bug_report" android:windowSoftInputMode="stateHidden|adjustResize" /> + + + diff --git a/vector/src/main/java/im/vector/KeyRequestHandler.java b/vector/src/main/java/im/vector/KeyRequestHandler.java index 191e50be3e..a112d6853c 100644 --- a/vector/src/main/java/im/vector/KeyRequestHandler.java +++ b/vector/src/main/java/im/vector/KeyRequestHandler.java @@ -319,7 +319,7 @@ private void displayKeyShareDialog(final MXSession session, final MXDeviceInfo d // set dialog message alertDialogBuilder - .setCancelable(true) + .setCancelable(false) .setNegativeButton(R.string.ignore_request, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { diff --git a/vector/src/main/java/im/vector/LoginHandler.java b/vector/src/main/java/im/vector/LoginHandler.java index d48e2ce571..b95b06a4aa 100644 --- a/vector/src/main/java/im/vector/LoginHandler.java +++ b/vector/src/main/java/im/vector/LoginHandler.java @@ -27,10 +27,10 @@ import org.matrix.androidsdk.rest.client.LoginRestClient; import org.matrix.androidsdk.rest.client.ThirdPidRestClient; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; import org.matrix.androidsdk.rest.model.login.Credentials; import org.matrix.androidsdk.rest.model.login.LoginFlow; import org.matrix.androidsdk.rest.model.login.RegistrationParams; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.ssl.CertUtil; import org.matrix.androidsdk.ssl.Fingerprint; import org.matrix.androidsdk.ssl.UnrecognizedCertificateException; @@ -157,7 +157,7 @@ private void callLogin(final Context ctx, final HomeServerConnectionConfig hsCon if (!TextUtils.isEmpty(username)) { if (android.util.Patterns.EMAIL_ADDRESS.matcher(username).matches()) { // Login with 3pid - client.loginWith3Pid(ThreePid.MEDIUM_EMAIL, username.toLowerCase(), password, deviceName, callback); + client.loginWith3Pid(ThreePid.MEDIUM_EMAIL, username.toLowerCase(VectorApp.getApplicationLocale()), password, deviceName, callback); } else { // Login with user client.loginWithUser(username, password, deviceName, callback); diff --git a/vector/src/main/java/im/vector/Matrix.java b/vector/src/main/java/im/vector/Matrix.java index c5649c0d99..083bf525f0 100755 --- a/vector/src/main/java/im/vector/Matrix.java +++ b/vector/src/main/java/im/vector/Matrix.java @@ -28,6 +28,7 @@ import org.matrix.androidsdk.crypto.MXCrypto; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.ssl.Fingerprint; import org.matrix.androidsdk.ssl.UnrecognizedCertificateException; import org.matrix.androidsdk.util.BingRulesManager; @@ -49,7 +50,6 @@ import im.vector.activity.CommonActivityUtils; import im.vector.activity.SplashActivity; -import im.vector.activity.VectorHomeActivity; import im.vector.gcm.GcmRegistrationManager; import im.vector.services.EventStreamService; import im.vector.store.LoginStorage; @@ -592,11 +592,16 @@ private MXSession createSession(final Context context, HomeServerConnectionConfi final MXSession session = new MXSession(hsConfig, new MXDataHandler(store, credentials), mAppContext); session.getDataHandler().setRequestNetworkErrorListener(new MXDataHandler.RequestNetworkErrorListener() { + @Override - public void onTokenCorrupted() { - if (null != VectorApp.getCurrentActivity()) { - Log.e(LOG_TAG, "## createSession() : onTokenCorrupted"); - CommonActivityUtils.logout(VectorApp.getCurrentActivity()); + public void onConfigurationError(String matrixErrorCode) { + Log.e(LOG_TAG, "## createSession() : onConfigurationError " + matrixErrorCode); + + if (TextUtils.equals(matrixErrorCode, MatrixError.UNKNOWN_TOKEN)) { + if (null != VectorApp.getCurrentActivity()) { + Log.e(LOG_TAG, "## createSession() : onTokenCorrupted"); + CommonActivityUtils.logout(VectorApp.getCurrentActivity()); + } } } diff --git a/vector/src/main/java/im/vector/PublicRoomsManager.java b/vector/src/main/java/im/vector/PublicRoomsManager.java index fa549ff249..d2e27bdaf6 100755 --- a/vector/src/main/java/im/vector/PublicRoomsManager.java +++ b/vector/src/main/java/im/vector/PublicRoomsManager.java @@ -25,8 +25,8 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.PublicRoom; -import org.matrix.androidsdk.rest.model.PublicRoomsResponse; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoomsResponse; import java.util.ArrayList; import java.util.List; diff --git a/vector/src/main/java/im/vector/RegistrationManager.java b/vector/src/main/java/im/vector/RegistrationManager.java index d03f63ec36..3cea84c4a1 100644 --- a/vector/src/main/java/im/vector/RegistrationManager.java +++ b/vector/src/main/java/im/vector/RegistrationManager.java @@ -28,7 +28,7 @@ import org.matrix.androidsdk.rest.client.ProfileRestClient; import org.matrix.androidsdk.rest.client.ThirdPidRestClient; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.rest.model.login.Credentials; import org.matrix.androidsdk.rest.model.login.LoginFlow; import org.matrix.androidsdk.rest.model.login.RegistrationFlowResponse; diff --git a/vector/src/main/java/im/vector/UnrecognizedCertHandler.java b/vector/src/main/java/im/vector/UnrecognizedCertHandler.java index 61f637af42..eaf1638c9a 100644 --- a/vector/src/main/java/im/vector/UnrecognizedCertHandler.java +++ b/vector/src/main/java/im/vector/UnrecognizedCertHandler.java @@ -73,7 +73,7 @@ public static void show(final HomeServerConnectionConfig hsConfig, final Fingerp TextView sslFingerprintTitle = layout.findViewById(R.id.ssl_fingerprint_title); sslFingerprintTitle.setText( - String.format(activity.getString(R.string.ssl_fingerprint_hash), unrecognizedFingerprint.getType().toString()) + String.format(VectorApp.getApplicationLocale(), activity.getString(R.string.ssl_fingerprint_hash), unrecognizedFingerprint.getType().toString()) ); TextView sslFingerprint = layout.findViewById(R.id.ssl_fingerprint); diff --git a/vector/src/main/java/im/vector/VectorApp.java b/vector/src/main/java/im/vector/VectorApp.java index 5b91d531ce..c50ef500c6 100755 --- a/vector/src/main/java/im/vector/VectorApp.java +++ b/vector/src/main/java/im/vector/VectorApp.java @@ -52,8 +52,6 @@ import org.piwik.sdk.extra.TrackHelper; import java.io.File; -import java.io.PrintWriter; -import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -79,7 +77,6 @@ import im.vector.contacts.PIDsRetriever; import im.vector.gcm.GcmRegistrationManager; import im.vector.services.EventStreamService; -import im.vector.util.BugReporter; import im.vector.util.CallsManager; import im.vector.util.PhoneNumberUtils; import im.vector.util.PreferencesManager; @@ -104,7 +101,7 @@ public class VectorApp extends MultiDexApplication { /** * Rage shake detection to send a bug report. */ - private static final RageShake mRageShake = new RageShake(); + private RageShake mRageShake; /** * Delay to detect if the application is in background. @@ -221,8 +218,6 @@ public void onCreate() { } catch (Exception e) { } - - mLogsDirectoryFile = new File(getCacheDir().getAbsolutePath() + "/logs"); org.matrix.androidsdk.util.Log.setLogDirectory(mLogsDirectoryFile); @@ -239,7 +234,7 @@ public void onCreate() { Log.d(LOG_TAG, "----------------------------------------------------------------"); Log.d(LOG_TAG, "----------------------------------------------------------------\n\n\n\n"); - mRageShake.start(this); + mRageShake = new RageShake(this); // init the REST client MXSession.initUserAgent(getApplicationContext()); @@ -414,6 +409,8 @@ private void suspendApp() { MyPresenceManager.advertiseAllUnavailable(); + mRageShake.stop(); + onAppPause(); } @@ -544,6 +541,7 @@ private void stopActivityTransitionTimer() { } MyPresenceManager.advertiseAllOnline(); + mRageShake.start(); mIsCallingInBackground = false; mIsInBackground = false; diff --git a/vector/src/main/java/im/vector/activity/AccountCreationCaptchaActivity.java b/vector/src/main/java/im/vector/activity/AccountCreationCaptchaActivity.java index 294219b94c..0241ed2e68 100755 --- a/vector/src/main/java/im/vector/activity/AccountCreationCaptchaActivity.java +++ b/vector/src/main/java/im/vector/activity/AccountCreationCaptchaActivity.java @@ -16,24 +16,32 @@ package im.vector.activity; +import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.Bitmap; import android.net.http.SslError; +import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import org.matrix.androidsdk.util.Log; import android.view.KeyEvent; +import android.view.View; import android.webkit.SslErrorHandler; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import im.vector.R; +import im.vector.util.VectorUtils; import java.net.URLDecoder; import java.util.Formatter; @@ -87,7 +95,8 @@ protected void onCreate(Bundle savedInstanceState) { final WebView webView = findViewById(R.id.account_creation_webview); webView.getSettings().setJavaScriptEnabled(true); - Intent intent = getIntent(); + final View loadingView = findViewById(R.id.account_creation_webview_loading); + final Intent intent = getIntent(); String homeServerUrl = "https://matrix.org/"; @@ -111,9 +120,14 @@ protected void onCreate(Bundle savedInstanceState) { webView.setWebViewClient(new WebViewClient() { @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, - SslError error) { - final SslErrorHandler fHander = handler; + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + loadingView.setVisibility(view.GONE); + } + + @Override + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { + Log.e(LOG_TAG, "## onReceivedSslError() : " + error.getCertificate()); AlertDialog.Builder builder = new AlertDialog.Builder(AccountCreationCaptchaActivity.this); @@ -122,14 +136,16 @@ public void onReceivedSslError(WebView view, SslErrorHandler handler, builder.setPositiveButton(R.string.ssl_trust, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - fHander.proceed(); + Log.d(LOG_TAG, "## onReceivedSslError() : the user trusted"); + handler.proceed(); } }); builder.setNegativeButton(R.string.ssl_do_not_trust, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - fHander.cancel(); + Log.d(LOG_TAG, "## onReceivedSslError() : the user did not trust"); + handler.cancel(); } }); @@ -137,7 +153,8 @@ public void onClick(DialogInterface dialog, int which) { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - fHander.cancel(); + handler.cancel(); + Log.d(LOG_TAG, "## onReceivedSslError() : the user dismisses the trust dialog."); dialog.dismiss(); return true; } @@ -149,10 +166,11 @@ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { dialog.show(); } - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - super.onReceivedError(view, errorCode, description, failingUrl); - + // common error message + private void onError(String errorMessage) { + Log.e(LOG_TAG, "## onError() : errorMessage"); + Toast.makeText(AccountCreationCaptchaActivity.this, errorMessage, Toast.LENGTH_LONG).show(); + // on error case, close this activity AccountCreationCaptchaActivity.this.runOnUiThread(new Runnable() { @Override @@ -162,6 +180,24 @@ public void run() { }); } + @Override + @SuppressLint("NewApi") + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onError(errorResponse.getReasonPhrase()); + } else { + onError(errorResponse.toString()); + } + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + onError(description); + } + @Override public boolean shouldOverrideUrlLoading(android.webkit.WebView view, java.lang.String url) { if ((null != url) && url.startsWith("js:")) { diff --git a/vector/src/main/java/im/vector/activity/CommonActivityUtils.java b/vector/src/main/java/im/vector/activity/CommonActivityUtils.java index a5ea5ec9b3..068bca5425 100755 --- a/vector/src/main/java/im/vector/activity/CommonActivityUtils.java +++ b/vector/src/main/java/im/vector/activity/CommonActivityUtils.java @@ -43,7 +43,7 @@ import android.os.Parcelable; import android.preference.PreferenceManager; import android.support.annotation.AttrRes; -import android.support.design.widget.Snackbar; +import android.support.annotation.ColorInt; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; @@ -481,15 +481,18 @@ private static boolean isUserLogout(Context aContext) { private static void sendEventStreamAction(Context context, EventStreamService.StreamAction action) { Context appContext = context.getApplicationContext(); - Log.d(LOG_TAG, "sendEventStreamAction " + action); - if (!isUserLogout(appContext)) { - // Fix https://github.com/vector-im/vector-android/issues/230 - // Only start the service if a session is in progress, otherwise - // starting the service is useless - Intent killStreamService = new Intent(appContext, EventStreamService.class); - killStreamService.putExtra(EventStreamService.EXTRA_STREAM_ACTION, action.ordinal()); - appContext.startService(killStreamService); + Intent eventStreamService = new Intent(appContext, EventStreamService.class); + + if ((action == EventStreamService.StreamAction.CATCHUP) && (EventStreamService.isStopped())) { + Log.d(LOG_TAG, "sendEventStreamAction : auto restart"); + eventStreamService.putExtra(EventStreamService.EXTRA_AUTO_RESTART_ACTION, EventStreamService.EXTRA_AUTO_RESTART_ACTION); + } else { + Log.d(LOG_TAG, "sendEventStreamAction " + action); + eventStreamService.putExtra(EventStreamService.EXTRA_STREAM_ACTION, action.ordinal()); + } + + appContext.startService(eventStreamService); } else { Log.d(LOG_TAG, "## sendEventStreamAction(): \"" + action + "\" action not sent - user logged out"); } @@ -594,6 +597,10 @@ public static void startEventStreamService(Context context) { context.startService(intent); } } + + if (null != EventStreamService.getInstance()) { + EventStreamService.getInstance().refreshStatusNotification(); + } } } @@ -1579,16 +1586,14 @@ public static void saveMediaIntoDownloads(final Context context, final File srcF saveFileInto(srcFile, Environment.DIRECTORY_DOWNLOADS, filename, new ApiCallback() { @Override public void onSuccess(String fullFilePath) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - if (null != fullFilePath) { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - - try { - File file = new File(fullFilePath); - downloadManager.addCompletedDownload(file.getName(), file.getName(), true, mimeType, file.getAbsolutePath(), file.length(), true); - } catch (Exception e) { - Log.e(LOG_TAG, "## saveMediaIntoDownloads(): Exception Msg=" + e.getMessage()); - } + if (null != fullFilePath) { + DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + + try { + File file = new File(fullFilePath); + downloadManager.addCompletedDownload(file.getName(), file.getName(), true, mimeType, file.getAbsolutePath(), file.length(), true); + } catch (Exception e) { + Log.e(LOG_TAG, "## saveMediaIntoDownloads(): Exception Msg=" + e.getMessage()); } } @@ -2055,18 +2060,28 @@ public static void tintMenuIcons(Menu menu, int color) { } /** - * Tint the drawable with the menu icon color + * Tint the drawable with a theme attribute * - * @param context the context - * @param drawable the drawable to tint + * @param context the context + * @param drawable the drawable to tint + * @param attribute the theme color * @return the tinted drawable */ public static Drawable tintDrawable(Context context, Drawable drawable, @AttrRes int attribute) { - int color = ThemeUtils.getColor(context, attribute); + return tintDrawableWithColor(drawable, ThemeUtils.getColor(context, attribute)); + } + + /** + * Tint the drawable with a color integer + * + * @param drawable the drawable to tint + * @param color the color + * @return the tinted drawable + */ + public static Drawable tintDrawableWithColor(Drawable drawable, @ColorInt int color) { Drawable tinted = DrawableCompat.wrap(drawable); drawable.mutate(); DrawableCompat.setTint(tinted, color); - return tinted; } } diff --git a/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.java b/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.java index b20c3f560c..8b620dda5a 100755 --- a/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.java +++ b/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.java @@ -22,7 +22,6 @@ import android.os.Build; import android.os.Bundle; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.webkit.ConsoleMessage; import android.webkit.JavascriptInterface; @@ -43,6 +42,7 @@ import org.matrix.androidsdk.rest.model.PowerLevels; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.util.JsonUtils; +import org.matrix.androidsdk.util.Log; import java.io.InputStream; import java.io.InputStreamReader; diff --git a/vector/src/main/java/im/vector/activity/LockScreenActivity.java b/vector/src/main/java/im/vector/activity/LockScreenActivity.java index 6f5622157f..896419f74b 100755 --- a/vector/src/main/java/im/vector/activity/LockScreenActivity.java +++ b/vector/src/main/java/im/vector/activity/LockScreenActivity.java @@ -42,7 +42,7 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import im.vector.Matrix; import im.vector.R; diff --git a/vector/src/main/java/im/vector/activity/LoginActivity.java b/vector/src/main/java/im/vector/activity/LoginActivity.java index faaf5d37d5..c122beba9f 100644 --- a/vector/src/main/java/im/vector/activity/LoginActivity.java +++ b/vector/src/main/java/im/vector/activity/LoginActivity.java @@ -57,7 +57,7 @@ import org.matrix.androidsdk.rest.client.LoginRestClient; import org.matrix.androidsdk.rest.client.ProfileRestClient; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.rest.model.login.Credentials; import org.matrix.androidsdk.rest.model.login.LoginFlow; import org.matrix.androidsdk.rest.model.login.RegistrationFlowResponse; @@ -1470,6 +1470,7 @@ private void hideMainLayoutAndToast(String text) { private void showMainLayout() { mMainLayout.setVisibility(View.VISIBLE); mProgressTextView.setVisibility(View.GONE); + mButtonsView.setVisibility(View.VISIBLE); } /** diff --git a/vector/src/main/java/im/vector/activity/MXCActionBarActivity.java b/vector/src/main/java/im/vector/activity/MXCActionBarActivity.java index 731d150ac6..e596c587f0 100755 --- a/vector/src/main/java/im/vector/activity/MXCActionBarActivity.java +++ b/vector/src/main/java/im/vector/activity/MXCActionBarActivity.java @@ -30,8 +30,6 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.data.Room; -import java.util.ArrayList; - import im.vector.Matrix; import im.vector.MyPresenceManager; import im.vector.R; diff --git a/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java b/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java index 36a11eea01..4a5f1ad484 100644 --- a/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java +++ b/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java @@ -40,7 +40,7 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.util.Log; import im.vector.Matrix; diff --git a/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java b/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java index 82234f59ba..9d2eb360eb 100644 --- a/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java +++ b/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java @@ -36,7 +36,7 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.util.Log; import im.vector.Matrix; diff --git a/vector/src/main/java/im/vector/activity/RoomDirectoryPickerActivity.java b/vector/src/main/java/im/vector/activity/RoomDirectoryPickerActivity.java index d8c192e9cc..2284ad47fa 100644 --- a/vector/src/main/java/im/vector/activity/RoomDirectoryPickerActivity.java +++ b/vector/src/main/java/im/vector/activity/RoomDirectoryPickerActivity.java @@ -38,8 +38,8 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThirdPartyProtocol; -import org.matrix.androidsdk.rest.model.ThirdPartyProtocolInstance; +import org.matrix.androidsdk.rest.model.pid.ThirdPartyProtocol; +import org.matrix.androidsdk.rest.model.pid.ThirdPartyProtocolInstance; import java.util.ArrayList; import java.util.Arrays; diff --git a/vector/src/main/java/im/vector/activity/SplashActivity.java b/vector/src/main/java/im/vector/activity/SplashActivity.java index 592911898d..7d61796051 100755 --- a/vector/src/main/java/im/vector/activity/SplashActivity.java +++ b/vector/src/main/java/im/vector/activity/SplashActivity.java @@ -36,7 +36,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.Map; /** * SplashActivity displays a splash while loading and inittializing the client. diff --git a/vector/src/main/java/im/vector/activity/VectorBaseSearchActivity.java b/vector/src/main/java/im/vector/activity/VectorBaseSearchActivity.java index 0bafd2a12d..47c98b027f 100644 --- a/vector/src/main/java/im/vector/activity/VectorBaseSearchActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorBaseSearchActivity.java @@ -27,7 +27,6 @@ import android.support.v7.app.ActionBar; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -37,6 +36,8 @@ import android.widget.EditText; import android.widget.TextView; +import org.matrix.androidsdk.util.Log; + import java.util.ArrayList; import java.util.List; import java.util.Timer; diff --git a/vector/src/main/java/im/vector/activity/VectorCallViewActivity.java b/vector/src/main/java/im/vector/activity/VectorCallViewActivity.java index c29c870155..2a67b73751 100755 --- a/vector/src/main/java/im/vector/activity/VectorCallViewActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorCallViewActivity.java @@ -63,7 +63,6 @@ import im.vector.Matrix; import im.vector.R; import im.vector.VectorApp; -import im.vector.services.EventStreamService; import im.vector.util.CallsManager; import im.vector.util.VectorUtils; import im.vector.view.VectorPendingCallView; @@ -466,11 +465,6 @@ public void onClick(View v) { } } } else { - Log.d(LOG_TAG, "## onCreate(): Hide the call notifications"); - if (null != EventStreamService.getInstance()) { - EventStreamService.getInstance().hideCallNotifications(); - } - // create the callview asap this.runOnUiThread(new Runnable() { @Override @@ -677,7 +671,6 @@ protected void onResume() { mHeaderPendingCallView.checkPendingCall(); - EventStreamService.getInstance().displayCallInProgressNotification(mCall.getSession(), mCall.getRoom(), mCall.getCallId()); // compute video UI layout position after rotation & apply new position computeVideoUiLayout(); if ((null != mCall) && mCall.isVideo() && mCall.getCallState().equals(IMXCall.CALL_STATE_CONNECTED)) { @@ -1121,7 +1114,7 @@ private void manageSubViews() { if (mCall.isIncoming()) { mCall.answer(); mIncomingCallTabbar.setVisibility(View.GONE); - } + } break; default: // nothing to do.. @@ -1248,7 +1241,7 @@ public void onSensorChanged(SensorEvent event) { if (null != event) { float distanceCentimeters = event.values[0]; - Log.d(LOG_TAG, "## onSensorChanged(): " + String.format("distance=%.3f", distanceCentimeters)); + Log.d(LOG_TAG, "## onSensorChanged(): " + String.format(VectorApp.getApplicationLocale(), "distance=%.3f", distanceCentimeters)); if (CallsManager.getSharedInstance().isSpeakerphoneOn()) { Log.d(LOG_TAG, "## onSensorChanged(): Skipped due speaker ON"); diff --git a/vector/src/main/java/im/vector/activity/VectorGroupDetailsActivity.java b/vector/src/main/java/im/vector/activity/VectorGroupDetailsActivity.java new file mode 100755 index 0000000000..8c551b4d6f --- /dev/null +++ b/vector/src/main/java/im/vector/activity/VectorGroupDetailsActivity.java @@ -0,0 +1,338 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.text.TextUtils; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ProgressBar; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.groups.GroupsManager; +import org.matrix.androidsdk.listeners.MXEventListener; +import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.util.Log; + +import java.util.List; + +import im.vector.Matrix; +import im.vector.R; +import im.vector.adapters.GroupDetailsFragmentPagerAdapter; +import im.vector.fragments.GroupDetailsBaseFragment; +import im.vector.util.ThemeUtils; +import im.vector.view.RiotViewPager; + +/** + * + */ +public class VectorGroupDetailsActivity extends MXCActionBarActivity { + private static final String LOG_TAG = VectorRoomDetailsActivity.class.getSimpleName(); + + // the group ID + public static final String EXTRA_GROUP_ID = "EXTRA_GROUP_ID"; + public static final String EXTRA_TAB_INDEX = "VectorUnifiedSearchActivity.EXTRA_TAB_INDEX"; + + // private classes + private MXSession mSession; + private GroupsManager mGroupsManager; + private Group mGroup; + + // UI views + private View mLoadingView; + private ProgressBar mGroupSyncInProgress; + + private RiotViewPager mPager; + private GroupDetailsFragmentPagerAdapter mPagerAdapter; + + private MXEventListener mGroupEventsListener = new MXEventListener() { + private void refresh(String groupId) { + if ((null != mGroup) && TextUtils.equals(mGroup.getGroupId(), groupId)) { + refreshGroupInfo(); + } + } + + @Override + public void onLeaveGroup(String groupId) { + if ((null != mRoom) && TextUtils.equals(groupId, mGroup.getGroupId())) { + VectorGroupDetailsActivity.this.finish(); + } + } + + @Override + public void onNewGroupInvitation(String groupId) { + refresh(groupId); + } + + @Override + public void onJoinGroup(String groupId) { + refresh(groupId); + } + + + @Override + public void onGroupProfileUpdate(String groupId) { + if ((null != mGroup) && TextUtils.equals(mGroup.getGroupId(), groupId)) { + if (null != mPagerAdapter.getHomeFragment()) { + mPagerAdapter.getHomeFragment().refreshViews(); + } + } + } + + @Override + public void onGroupRoomsListUpdate(String groupId) { + if ((null != mGroup) && TextUtils.equals(mGroup.getGroupId(), groupId)) { + if (null != mPagerAdapter.getRoomsFragment()) { + mPagerAdapter.getRoomsFragment().refreshViews(); + } + + if (null != mPagerAdapter.getHomeFragment()) { + mPagerAdapter.getHomeFragment().refreshViews(); + } + } + } + + @Override + public void onGroupUsersListUpdate(String groupId) { + if ((null != mGroup) && TextUtils.equals(mGroup.getGroupId(), groupId)) { + if (null != mPagerAdapter.getPeopleFragment()) { + mPagerAdapter.getPeopleFragment().refreshViews(); + } + + if (null != mPagerAdapter.getHomeFragment()) { + mPagerAdapter.getHomeFragment().refreshViews(); + } + } + } + + @Override + public void onGroupInvitedUsersListUpdate(String groupId) { + onGroupUsersListUpdate(groupId); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (CommonActivityUtils.shouldRestartApp(this)) { + Log.e(LOG_TAG, "Restart the application."); + CommonActivityUtils.restartApp(this); + return; + } + + if (CommonActivityUtils.isGoingToSplash(this)) { + Log.d(LOG_TAG, "onCreate : Going to splash screen"); + return; + } + + Intent intent = getIntent(); + + if (!intent.hasExtra(EXTRA_GROUP_ID)) { + Log.e(LOG_TAG, "No group id"); + finish(); + return; + } + + // get current session + mSession = Matrix.getInstance(getApplicationContext()).getSession(intent.getStringExtra(EXTRA_MATRIX_ID)); + + if ((null == mSession) || !mSession.isAlive()) { + finish(); + return; + } + + mGroupsManager = mSession.getGroupsManager(); + + String groupId = intent.getStringExtra(EXTRA_GROUP_ID); + + if (!MXSession.isGroupId(groupId)) { + Log.e(LOG_TAG, "invalid group id " + groupId); + finish(); + return; + } + + mGroup = mGroupsManager.getGroup(groupId); + + if (null == mGroup) { + Log.d(LOG_TAG, "## onCreate() : displaying " + groupId + " in preview mode"); + mGroup = new Group(groupId); + } else { + Log.d(LOG_TAG, "## onCreate() : displaying " + groupId); + } + + setContentView(R.layout.activity_vector_group_details); + + // UI widgets binding & init fields + mLoadingView = findViewById(R.id.group_loading_layout); + + // tab creation and restore tabs UI context + ActionBar actionBar = getSupportActionBar(); + + if (null != actionBar) { + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + + mGroupSyncInProgress = findViewById(R.id.group_sync_in_progress); + + mPager = findViewById(R.id.groups_pager); + mPagerAdapter = new GroupDetailsFragmentPagerAdapter(getSupportFragmentManager(), this); + mPager.setAdapter(mPagerAdapter); + + TabLayout layout = findViewById(R.id.group_tabs); + ThemeUtils.setTabLayoutTheme(this, layout); + + if (intent.hasExtra(EXTRA_TAB_INDEX)) { + mPager.setCurrentItem(getIntent().getIntExtra(EXTRA_TAB_INDEX, 0)); + } else { + mPager.setCurrentItem((null != savedInstanceState) ? savedInstanceState.getInt(EXTRA_TAB_INDEX, 0) : 0); + } + layout.setupWithViewPager(mPager); + + mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // dismiss the keyboard when swiping + final View view = getCurrentFocus(); + if (view != null) { + final InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + @Override + public void onPageSelected(int position) { + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + } + + /** + * @return the used group + */ + public Group getGroup() { + return mGroup; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + List allFragments = getSupportFragmentManager().getFragments(); + + // dispatch the result to each fragments + for (android.support.v4.app.Fragment fragment : allFragments) { + fragment.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(EXTRA_TAB_INDEX, mPager.getCurrentItem()); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onPause() { + super.onPause(); + mSession.getDataHandler().removeListener(mGroupEventsListener); + } + + @Override + protected void onResume() { + super.onResume(); + refreshGroupInfo(); + mSession.getDataHandler().addListener(mGroupEventsListener); + } + + /** + * SHow the waiting view + */ + public void showWaitingView() { + if (null != mLoadingView) { + mLoadingView.setVisibility(View.VISIBLE); + } + } + + /** + * Hide the waiting view + */ + public void stopWaitingView() { + if (null != mLoadingView) { + mLoadingView.setVisibility(View.GONE); + } + } + + /** + * Refresh the group information + */ + private void refreshGroupInfo() { + if (null != mGroup) { + mGroupSyncInProgress.setVisibility(View.VISIBLE); + mGroupsManager.refreshGroupData(mGroup, new ApiCallback() { + private void onDone() { + if (null != mGroupSyncInProgress) { + mGroupSyncInProgress.setVisibility(View.GONE); + } + } + + @Override + public void onSuccess(Void info) { + onDone(); + } + + @Override + public void onNetworkError(Exception e) { + onDone(); + } + + @Override + public void onMatrixError(MatrixError e) { + onDone(); + } + + @Override + public void onUnexpectedError(Exception e) { + onDone(); + } + }); + } + } +} diff --git a/vector/src/main/java/im/vector/activity/VectorHomeActivity.java b/vector/src/main/java/im/vector/activity/VectorHomeActivity.java index a94ada3bde..5a2f5fb392 100644 --- a/vector/src/main/java/im/vector/activity/VectorHomeActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorHomeActivity.java @@ -33,7 +33,6 @@ import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.provider.Settings; import android.support.annotation.NonNull; import android.support.design.internal.BottomNavigationItemView; import android.support.design.internal.BottomNavigationMenuView; @@ -110,6 +109,7 @@ import im.vector.VectorApp; import im.vector.fragments.AbsHomeFragment; import im.vector.fragments.FavouritesFragment; +import im.vector.fragments.GroupsFragment; import im.vector.fragments.HomeFragment; import im.vector.fragments.PeopleFragment; import im.vector.fragments.RoomsFragment; @@ -117,7 +117,6 @@ import im.vector.services.EventStreamService; import im.vector.util.BugReporter; import im.vector.util.CallsManager; -import im.vector.util.PreferencesManager; import im.vector.util.RoomUtils; import im.vector.util.ThemeUtils; import im.vector.util.VectorUtils; @@ -141,6 +140,9 @@ public class VectorHomeActivity extends RiotAppCompatActivity implements SearchV // jump to a member details sheet public static final String EXTRA_MEMBER_ID = "VectorHomeActivity.EXTRA_MEMBER_ID"; + // jump to a group details sheet + public static final String EXTRA_GROUP_ID = "VectorHomeActivity.EXTRA_GROUP_ID"; + // there are two ways to open an external link // 1- EXTRA_UNIVERSAL_LINK_URI : the link is opened as soon there is an event check processed (application is launched when clicking on the URI link) // 2- EXTRA_JUMP_TO_UNIVERSAL_LINK : do not wait that an event chunk is processed. @@ -166,6 +168,7 @@ public class VectorHomeActivity extends RiotAppCompatActivity implements SearchV private static final String TAG_FRAGMENT_FAVOURITES = "TAG_FRAGMENT_FAVOURITES"; private static final String TAG_FRAGMENT_PEOPLE = "TAG_FRAGMENT_PEOPLE"; private static final String TAG_FRAGMENT_ROOMS = "TAG_FRAGMENT_ROOMS"; + private static final String TAG_FRAGMENT_GROUPS = "TAG_FRAGMENT_GROUPS"; // Key used to restore the proper fragment after orientation change private static final String CURRENT_MENU_ID = "CURRENT_MENU_ID"; @@ -177,6 +180,8 @@ public class VectorHomeActivity extends RiotAppCompatActivity implements SearchV private String mMemberIdToOpen = null; + private String mGroupIdToOpen = null; + @BindView(R.id.listView_spinner_views) View mWaitingView; @@ -311,6 +316,7 @@ protected void onCreate(Bundle savedInstanceState) { intent.removeExtra(EXTRA_JUMP_TO_UNIVERSAL_LINK); intent.removeExtra(EXTRA_JUMP_TO_ROOM_PARAMS); intent.removeExtra(EXTRA_MEMBER_ID); + intent.removeExtra(EXTRA_GROUP_ID); intent.removeExtra(VectorUniversalLinkReceiver.EXTRA_UNIVERSAL_LINK_URI); } else { @@ -339,6 +345,9 @@ protected void onCreate(Bundle savedInstanceState) { mMemberIdToOpen = intent.getStringExtra(EXTRA_MEMBER_ID); intent.removeExtra(EXTRA_MEMBER_ID); + mGroupIdToOpen = intent.getStringExtra(EXTRA_GROUP_ID); + intent.removeExtra(EXTRA_GROUP_ID); + // the home activity has been launched with an universal link if (intent.hasExtra(VectorUniversalLinkReceiver.EXTRA_UNIVERSAL_LINK_URI)) { Log.d(LOG_TAG, "Has an universal link"); @@ -450,6 +459,19 @@ public void run() { initViews(); } + /** + * Display the TAB if it is required + */ + private void showFloatingActionButton() { + if (null != mFloatingActionButton) { + if ((mCurrentMenuId == R.id.bottom_action_favourites) || (mCurrentMenuId == R.id.bottom_action_groups)) { + mFloatingActionButton.setVisibility(View.GONE); + } else { + mFloatingActionButton.show(); + } + } + } + @Override protected void onResume() { super.onResume(); @@ -487,13 +509,7 @@ public void run() { addEventsListener(); } - if (null != mFloatingActionButton) { - if (mCurrentMenuId == R.id.bottom_action_favourites) { - mFloatingActionButton.setVisibility(View.GONE); - } else { - mFloatingActionButton.show(); - } - } + showFloatingActionButton(); this.runOnUiThread(new Runnable() { @Override @@ -539,6 +555,14 @@ public void onClick(DialogInterface dialog, int which) { mMemberIdToOpen = null; } + if (null != mGroupIdToOpen) { + Intent groupIntent = new Intent(VectorHomeActivity.this, VectorGroupDetailsActivity.class); + groupIntent.putExtra(VectorGroupDetailsActivity.EXTRA_GROUP_ID, mGroupIdToOpen); + groupIntent.putExtra(VectorGroupDetailsActivity.EXTRA_MATRIX_ID, mSession.getCredentials().userId); + startActivity(groupIntent); + mGroupIdToOpen = null; + } + // https://github.com/vector-im/vector-android/issues/323 // the tool bar color is not restored on some devices. TypedValue vectorActionBarColor = new TypedValue(); @@ -690,6 +714,8 @@ protected void onNewIntent(Intent intent) { mMemberIdToOpen = intent.getStringExtra(EXTRA_MEMBER_ID); intent.removeExtra(EXTRA_MEMBER_ID); + mGroupIdToOpen = intent.getStringExtra(EXTRA_GROUP_ID); + intent.removeExtra(EXTRA_GROUP_ID); // start waiting view if (intent.getBooleanExtra(EXTRA_WAITING_VIEW_STATUS, VectorHomeActivity.WAITING_VIEW_STOP)) { @@ -785,6 +811,15 @@ private void updateSelectedFragment(final MenuItem item) { mCurrentFragmentTag = TAG_FRAGMENT_ROOMS; mSearchView.setQueryHint(getString(R.string.home_filter_placeholder_rooms)); break; + case R.id.bottom_action_groups: + Log.d(LOG_TAG, "onNavigationItemSelected GROUPS"); + fragment = mFragmentManager.findFragmentByTag(TAG_FRAGMENT_GROUPS); + if (fragment == null) { + fragment = GroupsFragment.newInstance(); + } + mCurrentFragmentTag = TAG_FRAGMENT_GROUPS; + mSearchView.setQueryHint(getString(R.string.home_filter_placeholder_groups)); + break; } synchronized (this) { @@ -792,17 +827,15 @@ private void updateSelectedFragment(final MenuItem item) { mFloatingActionButtonTimer.cancel(); mFloatingActionButtonTimer = null; } - mFloatingActionButton.show(); } // clear waiting view stopWaitingView(); - // don't display the fab for the favorites tab - mFloatingActionButton.setVisibility((item.getItemId() != R.id.bottom_action_favourites) ? View.VISIBLE : View.GONE); - mCurrentMenuId = item.getItemId(); + showFloatingActionButton(); + if (fragment != null) { resetFilter(); try { @@ -896,7 +929,11 @@ public void run() { mFloatingActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - onFloatingButtonClick(); + Fragment fragment = getSelectedFragment(); + + if (!(fragment instanceof AbsHomeFragment) || !((AbsHomeFragment) fragment).onFabClick()) { + onFloatingButtonClick(); + } } }); } @@ -1008,6 +1045,10 @@ private Fragment getSelectedFragment() { case R.id.bottom_action_rooms: fragment = mFragmentManager.findFragmentByTag(TAG_FRAGMENT_ROOMS); break; + case R.id.bottom_action_groups: + fragment = mFragmentManager.findFragmentByTag(TAG_FRAGMENT_GROUPS); + break; + } return fragment; @@ -1129,12 +1170,6 @@ public void onClick(DialogInterface d, int n) { } } }) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - invitePeopleToNewRoom(); - } - }) .setNegativeButton(R.string.cancel, null) .show(); } @@ -1173,9 +1208,7 @@ public void run() { VectorHomeActivity.this.runOnUiThread(new Runnable() { @Override public void run() { - if (null != mFloatingActionButton) { - mFloatingActionButton.show(); - } + showFloatingActionButton(); } }); } @@ -1191,9 +1224,7 @@ public void run() { VectorHomeActivity.this.runOnUiThread(new Runnable() { @Override public void run() { - if (null != mFloatingActionButton) { - mFloatingActionButton.show(); - } + showFloatingActionButton(); } }); @@ -1448,50 +1479,84 @@ public void onPreviewRoom(MXSession session, String roomId) { CommonActivityUtils.previewRoom(this, roomPreviewData); } - public void onRejectInvitation(final MXSession session, final String roomId) { - Room room = session.getDataHandler().getRoom(roomId); - - if (null != room) { - showWaitingView(); + /** + * Create the room forget / leave callback + * + * @param roomId the room id + * @param onSuccessCallback the success callback + * @return the asynchronous callback + */ + private ApiCallback getForgetLeaveCallback(final String roomId, final SimpleApiCallback onSuccessCallback) { + return new ApiCallback() { + @Override + public void onSuccess(Void info) { + runOnUiThread(new Runnable() { + @Override + public void run() { + // clear any pending notification for this room + EventStreamService.cancelNotificationsForRoomId(mSession.getMyUserId(), roomId); + stopWaitingView(); - room.leave(new ApiCallback() { - @Override - public void onSuccess(Void info) { - runOnUiThread(new Runnable() { - @Override - public void run() { - // clear any pending notification for this room - EventStreamService.cancelNotificationsForRoomId(mSession.getMyUserId(), roomId); - stopWaitingView(); + if (null != onSuccessCallback) { + onSuccessCallback.onSuccess(null); } - }); - } + } + }); + } - private void onError(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - stopWaitingView(); - Toast.makeText(VectorHomeActivity.this, message, Toast.LENGTH_LONG).show(); - } - }); - } + private void onError(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + stopWaitingView(); + Toast.makeText(VectorHomeActivity.this, message, Toast.LENGTH_LONG).show(); + } + }); + } - @Override - public void onNetworkError(Exception e) { - onError(e.getLocalizedMessage()); - } + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } - @Override - public void onMatrixError(MatrixError e) { - onError(e.getLocalizedMessage()); - } + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } - @Override - public void onUnexpectedError(Exception e) { - onError(e.getLocalizedMessage()); - } - }); + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }; + } + + /** + * Trigger the room forget + * @param roomId the room id + * @param onSuccessCallback the success asynchronous callback + */ + public void onForgetRoom(final String roomId, final SimpleApiCallback onSuccessCallback) { + Room room = mSession.getDataHandler().getRoom(roomId); + + if (null != room) { + showWaitingView(); + room.forget(getForgetLeaveCallback(roomId, onSuccessCallback)); + } + } + + /** + * Trigger the room leave / invitation reject. + * + * @param roomId the room id + * @param onSuccessCallback the success asynchronous callback + */ + public void onRejectInvitation(final String roomId, final SimpleApiCallback onSuccessCallback) { + Room room = mSession.getDataHandler().getRoom(roomId); + + if (null != room) { + showWaitingView(); + room.leave(getForgetLeaveCallback(roomId, onSuccessCallback)); } } @@ -1630,6 +1695,21 @@ public void onDrawerClosed(View view) { break; } + case R.id.sliding_menu_exit : { + if (null != EventStreamService.getInstance()) { + EventStreamService.getInstance().stopNow(); + } + VectorHomeActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + VectorHomeActivity.this.finish(); + System.exit(0); + } + }); + + break; + } + case R.id.sliding_menu_sign_out: { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(VectorHomeActivity.this); alertDialogBuilder.setMessage(getString(R.string.action_sign_out_confirmation)); diff --git a/vector/src/main/java/im/vector/activity/VectorMediasViewerActivity.java b/vector/src/main/java/im/vector/activity/VectorMediasViewerActivity.java index 8c24a3052f..7290c61cde 100755 --- a/vector/src/main/java/im/vector/activity/VectorMediasViewerActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorMediasViewerActivity.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.os.Bundle; import android.support.v4.view.ViewPager; -import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -37,8 +36,6 @@ import org.matrix.androidsdk.util.Log; import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; import java.util.List; import im.vector.Matrix; @@ -75,6 +72,8 @@ public class VectorMediasViewerActivity extends MXCActionBarActivity { // the medias list private List mMediasList; + private MenuItem mShareMenuItem; + // the slide effect public class DepthPageTransformer implements ViewPager.PageTransformer { private static final float MIN_SCALE = 0.75f; @@ -176,6 +175,11 @@ public void onPageSelected(int position) { if (null != VectorMediasViewerActivity.this.getSupportActionBar()) { VectorMediasViewerActivity.this.getSupportActionBar().setTitle(mMediasList.get(position).mFileName); } + + // disable shared for encrypted files as they are saved in a tmp folder + if (null != mShareMenuItem) { + mShareMenuItem.setVisible(null == mMediasList.get(position).mEncryptedFileInfo); + } } @Override @@ -204,6 +208,11 @@ public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.vector_medias_viewer, menu); CommonActivityUtils.tintMenuIcons(menu, ThemeUtils.getColor(this, R.attr.icon_tint_on_dark_action_bar_color)); + mShareMenuItem = menu.findItem(R.id.ic_action_share); + if (null != mShareMenuItem) { + mShareMenuItem.setVisible(null == mMediasList.get(mViewPager.getCurrentItem()).mEncryptedFileInfo); + } + return true; } @@ -212,60 +221,60 @@ public boolean onCreateOptionsMenu(Menu menu) { */ private void onAction(final int position, final int action) { MXMediasCache mediasCache = Matrix.getInstance(this).getMediasCache(); - SlidableMediaInfo mediaInfo = mMediasList.get(position); - - File file = mediasCache.mediaCacheFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType); + final SlidableMediaInfo mediaInfo = mMediasList.get(position); // check if the media has already been downloaded - if (null != file) { - // download - if (action == R.id.ic_action_download) { - CommonActivityUtils.saveMediaIntoDownloads(this, file, mediaInfo.mFileName, mediaInfo.mMimeType, new SimpleApiCallback() { - @Override - public void onSuccess(String string) { - Toast.makeText(VectorApp.getInstance(), getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + if (mediasCache.isMediaCached(mediaInfo.mMediaUrl, mediaInfo.mMimeType)) { + mediasCache.createTmpMediaFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File file) { + // sanity check + if (null == file) { + return; } - }); - } else { - // shared - Uri mediaUri = null; - - File renamedFile = file; - if (!TextUtils.isEmpty(mediaInfo.mFileName)) - try { - InputStream fin = new FileInputStream(file); - String tmpUrl = mediasCache.saveMedia(fin, mediaInfo.mFileName, mediaInfo.mMimeType); - - if (null != tmpUrl) { - renamedFile = mediasCache.mediaCacheFile(tmpUrl, mediaInfo.mMimeType); + if (action == R.id.ic_action_download) { + CommonActivityUtils.saveMediaIntoDownloads(VectorMediasViewerActivity.this, file, mediaInfo.mFileName, mediaInfo.mMimeType, new SimpleApiCallback() { + @Override + public void onSuccess(String savedMediaPath) { + Toast.makeText(VectorApp.getInstance(), getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + } + }); + } else { + if (null != mediaInfo.mFileName) { + File dstFile = new File(file.getParent(), mediaInfo.mFileName); + + if (dstFile.exists()) { + dstFile.delete(); + } + + file.renameTo(dstFile); + file = dstFile; } - } catch (Exception e) { - Log.e(LOG_TAG, "## onAction() : mediasCache.mediaCacheFile.absolutePathToUri failed " + e.getMessage()); - } + // shared / forward + Uri mediaUri = null; + try { + mediaUri = VectorContentProvider.absolutePathToUri(VectorMediasViewerActivity.this, file.getAbsolutePath()); + } catch (Exception e) { + Log.e(LOG_TAG, "onMediaAction onAction.absolutePathToUri: " + e.getMessage()); + } - if (null != renamedFile) { - try { - mediaUri = VectorContentProvider.absolutePathToUri(this, renamedFile.getAbsolutePath()); - } catch (Exception e) { - Log.e(LOG_TAG, "## onAction() : RiotContentProvider.absolutePathToUri failed " + e.getMessage()); - } - } - - if (null != mediaUri) { - try { - final Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.setType(mediaInfo.mMimeType); - sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); - startActivity(sendIntent); - } catch (Exception e) { - Log.e(LOG_TAG, "## onAction : cannot display the media " + mediaUri + " mimeType " + mediaInfo.mMimeType); - CommonActivityUtils.displayToast(this, e.getLocalizedMessage()); + if (null != mediaUri) { + try { + final Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.setType(mediaInfo.mMimeType); + sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); + startActivity(sendIntent); + } catch (Exception e) { + Log.e(LOG_TAG, "## onAction : cannot display the media " + mediaUri + " mimeType " + mediaInfo.mMimeType); + CommonActivityUtils.displayToast(VectorMediasViewerActivity.this, e.getLocalizedMessage()); + } + } } } - } + }); } else { // else download it final String downloadId = mediasCache.downloadMedia(this, mSession.getHomeServerConfig(), mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo); diff --git a/vector/src/main/java/im/vector/activity/VectorMemberDetailsActivity.java b/vector/src/main/java/im/vector/activity/VectorMemberDetailsActivity.java index 24938f1091..e64c53d201 100644 --- a/vector/src/main/java/im/vector/activity/VectorMemberDetailsActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorMemberDetailsActivity.java @@ -58,7 +58,6 @@ import im.vector.Matrix; import im.vector.R; -import im.vector.VectorApp; import im.vector.adapters.VectorMemberDetailsAdapter; import im.vector.adapters.VectorMemberDetailsDevicesAdapter; import im.vector.fragments.VectorUnknownDevicesFragment; @@ -86,7 +85,7 @@ public class VectorMemberDetailsActivity extends MXCActionBarActivity implements private static final int ITEM_ACTION_INVITE = 0; private static final int ITEM_ACTION_LEAVE = 1; public static final int ITEM_ACTION_KICK = 2; - private static final int ITEM_ACTION_BAN = 3; + public static final int ITEM_ACTION_BAN = 3; private static final int ITEM_ACTION_UNBAN = 4; private static final int ITEM_ACTION_IGNORE = 5; private static final int ITEM_ACTION_UNIGNORE = 6; @@ -378,21 +377,41 @@ public void performItemAction(final int aActionType) { final ArrayList idsList = new ArrayList<>(); + String displayName = (null == mRoomMember) ? mMemberId : (TextUtils.isEmpty(mRoomMember.displayname) ? mRoomMember.getUserId() : mRoomMember.displayname); + switch (aActionType) { case ITEM_ACTION_DEVICES: refreshDevicesListView(); break; case ITEM_ACTION_START_CHAT: - Log.d(LOG_TAG, "## performItemAction(): Start new room - start chat"); + android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(this); + builder.setTitle(R.string.dialog_title_confirmation); - VectorMemberDetailsActivity.this.runOnUiThread(new Runnable() { + builder.setMessage(getString(R.string.start_new_chat_prompt_msg, displayName)); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override - public void run() { - enableProgressBarView(CommonActivityUtils.UTILS_DISPLAY_PROGRESS_BAR); - mSession.createDirectMessageRoom(mMemberId, mCreateDirectMessageCallBack); + public void onClick(DialogInterface dialog, int which) { + Log.d(LOG_TAG, "## performItemAction(): Start new room - start chat"); + + VectorMemberDetailsActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + enableProgressBarView(CommonActivityUtils.UTILS_DISPLAY_PROGRESS_BAR); + mSession.createDirectMessageRoom(mMemberId, mCreateDirectMessageCallBack); + } + }); } }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // nothing to do + } + }); + + builder.show(); break; case ITEM_ACTION_START_VIDEO_CALL: @@ -592,8 +611,6 @@ public void onClick(DialogInterface dialog, int id) { break; } case ITEM_ACTION_MENTION: - String displayName = TextUtils.isEmpty(mRoomMember.displayname) ? mRoomMember.getUserId() : mRoomMember.displayname; - // provide the mention name Intent intent = new Intent(); intent.putExtra(RESULT_MENTION_ID, displayName); diff --git a/vector/src/main/java/im/vector/activity/VectorRoomActivity.java b/vector/src/main/java/im/vector/activity/VectorRoomActivity.java index 9309e57e9f..adb33f1da8 100755 --- a/vector/src/main/java/im/vector/activity/VectorRoomActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorRoomActivity.java @@ -34,6 +34,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Vibrator; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.support.annotation.ColorInt; @@ -85,9 +86,9 @@ import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.rest.model.PowerLevels; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.JsonUtils; @@ -111,9 +112,9 @@ import im.vector.ViewedRoomTracker; import im.vector.fragments.VectorMessageListFragment; import im.vector.fragments.VectorUnknownDevicesFragment; +import im.vector.notifications.NotificationUtils; import im.vector.services.EventStreamService; import im.vector.util.CallsManager; -import im.vector.util.NotificationUtils; import im.vector.util.PreferencesManager; import im.vector.util.ReadMarkerManager; import im.vector.util.SlashComandsParser; @@ -399,6 +400,21 @@ public void run() { }); } + @Override + public void onRoomKick(String roomId) { + HashMap params = new HashMap<>(); + + params.put(VectorRoomActivity.EXTRA_MATRIX_ID, mSession.getMyUserId()); + params.put(VectorRoomActivity.EXTRA_ROOM_ID, mRoom.getRoomId()); + + // clear the activity stack to home activity + Intent intent = new Intent(VectorRoomActivity.this, VectorHomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + intent.putExtra(VectorHomeActivity.EXTRA_JUMP_TO_ROOM_PARAMS, params); + VectorRoomActivity.this.startActivity(intent); + } + @Override public void onLiveEvent(final Event event, RoomState roomState) { VectorRoomActivity.this.runOnUiThread(new Runnable() { @@ -691,7 +707,7 @@ public void onClick(View v) { final Integer[] icons; if (PreferencesManager.useNativeCamera(VectorRoomActivity.this)) { - messages = new Integer[]{ + messages = new Integer[]{ R.string.option_send_files, R.string.option_take_photo, R.string.option_take_video, @@ -703,7 +719,7 @@ public void onClick(View v) { R.drawable.ic_material_videocam }; } else { - messages = new Integer[]{ + messages = new Integer[]{ R.string.option_send_files, R.string.option_take_photo_video }; @@ -910,14 +926,16 @@ public void onClick(View v) { mVectorRoomMediasSender = new VectorRoomMediasSender(this, mVectorMessageListFragment, Matrix.getInstance(this).getMediasCache()); manageRoomPreview(); - addRoomHeaderClickListeners(); + RoomMember member = (null != mRoom) ? mRoom.getMember(mMyUserId) : null; + boolean hasBeenKicked = (null != member) && member.kickedOrBanned(); + // in timeline mode (i.e search in the forward and backward room history) // or in room preview mode // the edition items are not displayed - if ((!TextUtils.isEmpty(mEventId) || (null != sRoomPreviewData))) { - if (!mIsUnreadPreviewMode) { + if ((!TextUtils.isEmpty(mEventId) || (null != sRoomPreviewData)) || hasBeenKicked) { + if (!mIsUnreadPreviewMode || hasBeenKicked) { mNotificationsArea.setVisibility(View.GONE); findViewById(R.id.bottom_separator).setVisibility(View.GONE); findViewById(R.id.room_notification_separator).setVisibility(View.GONE); @@ -930,6 +948,10 @@ public void onClick(View v) { v.setLayoutParams(params); } + if ((null == sRoomPreviewData) && hasBeenKicked) { + manageBannedHeader(member); + } + mLatestChatMessageCache = Matrix.getInstance(this).getDefaultLatestChatMessageCache(); // some medias must be sent while opening the chat @@ -1040,12 +1062,6 @@ public void onClick(DialogInterface d, int n) { displayWidget(widgets.get(n)); } }) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - displayWidget(widgets.get(0)); - } - }) .setNegativeButton(R.string.cancel, null) .show(); } @@ -1057,7 +1073,7 @@ public void onClick(DialogInterface dialog, int which) { private void startCall(boolean isVideo) { if (CommonActivityUtils.checkPermissions(isVideo ? CommonActivityUtils.REQUEST_CODE_PERMISSION_VIDEO_IP_CALL : CommonActivityUtils.REQUEST_CODE_PERMISSION_AUDIO_IP_CALL, VectorRoomActivity.this)) { - startIpCall(isVideo); + startIpCall(false, isVideo); } } @@ -1302,8 +1318,8 @@ protected void onResume() { mVectorMessageListFragment.refresh(); // the list automatically scrolls down when its top moves down - if (mVectorMessageListFragment.mMessageListView instanceof AutoScrollDownListView) { - ((AutoScrollDownListView) mVectorMessageListFragment.mMessageListView).lockSelectionOnResize(); + if (null != mVectorMessageListFragment.mMessageListView) { + mVectorMessageListFragment.mMessageListView.lockSelectionOnResize(); } // the device has been rotated @@ -1555,6 +1571,13 @@ public boolean onCreateOptionsMenu(Menu menu) { mSearchInRoomMenuItem = menu.findItem(R.id.ic_action_search_in_room); mUseMatrixAppsMenuItem = menu.findItem(R.id.ic_action_matrix_apps); + RoomMember member = mRoom.getMember(mSession.getMyUserId()); + + // kicked / banned room + if ((null != member) && member.kickedOrBanned()) { + menu.findItem(R.id.ic_action_room_leave).setVisible(false); + } + // hide / show the unsent / resend all entries. refreshNotificationsArea(); } @@ -1726,9 +1749,35 @@ public void onItemClick(IconAndTextDialogFragment dialogFragment, int position) requestCode = CommonActivityUtils.REQUEST_CODE_PERMISSION_VIDEO_IP_CALL; } - if (CommonActivityUtils.checkPermissions(requestCode, VectorRoomActivity.this)) { - startIpCall(isVideoCall); + final boolean finalIsVideoCall = isVideoCall; + final int finalRequestCode = requestCode; + + android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(VectorRoomActivity.this); + builder.setTitle(R.string.dialog_title_confirmation); + + if (finalIsVideoCall) { + builder.setMessage(getString(R.string.start_video_call_prompt_msg)); + } else { + builder.setMessage(getString(R.string.start_voice_call_prompt_msg)); } + + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (CommonActivityUtils.checkPermissions(finalRequestCode, VectorRoomActivity.this)) { + startIpCall(PreferencesManager.useJitsiConfCall(VectorRoomActivity.this), finalIsVideoCall); + } + } + }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // nothing to do + } + }); + + builder.show(); } }); @@ -1794,10 +1843,11 @@ public void onUnexpectedError(Exception e) { * Start an IP call: audio call if aIsVideoCall is false or video call if aIsVideoCall * is true. * + * @param useJitsiCall true to use jitsi calls * @param aIsVideoCall true to video call, false to audio call */ - private void startIpCall(final boolean aIsVideoCall) { - if ((mRoom.getActiveMembers().size() > 2) && PreferencesManager.useJitsiConfCall(this)) { + private void startIpCall(final boolean useJitsiCall, final boolean aIsVideoCall) { + if ((mRoom.getActiveMembers().size() > 2) && useJitsiCall) { startJitsiCall(aIsVideoCall); return; } @@ -1858,7 +1908,7 @@ public void onMatrixError(MatrixError e) { CommonActivityUtils.displayUnknownDevicesDialog(mSession, VectorRoomActivity.this, (MXUsersDevicesMap) cryptoError.mExceptionData, new VectorUnknownDevicesFragment.IUnknownDevicesSendAnywayListener() { @Override public void onSendAnyway() { - startIpCall(aIsVideoCall); + startIpCall(useJitsiCall, aIsVideoCall); } }); @@ -2241,12 +2291,10 @@ private void launchNativeCamera() { if (null == dummyUri) { Log.e(LOG_TAG, "Cannot use the external storage media to save image"); } - } - catch (UnsupportedOperationException uoe) { + } catch (UnsupportedOperationException uoe) { Log.e(LOG_TAG, "Unable to insert camera URI into MediaStore.Images.Media.EXTERNAL_CONTENT_URI - no SD card? Attempting to insert into device storage."); - } - catch (Exception e) { - Log.e(LOG_TAG, "Unable to insert camera URI into MediaStore.Images.Media.EXTERNAL_CONTENT_URI. "+e); + } catch (Exception e) { + Log.e(LOG_TAG, "Unable to insert camera URI into MediaStore.Images.Media.EXTERNAL_CONTENT_URI. " + e); } if (null == dummyUri) { @@ -2356,11 +2404,11 @@ public void onRequestPermissionsResult(int aRequestCode, @NonNull String[] aPerm } } else if (aRequestCode == CommonActivityUtils.REQUEST_CODE_PERMISSION_AUDIO_IP_CALL) { if (CommonActivityUtils.onPermissionResultAudioIpCall(this, aPermissions, aGrantResults)) { - startIpCall(false); + startIpCall(PreferencesManager.useJitsiConfCall(this), false); } } else if (aRequestCode == CommonActivityUtils.REQUEST_CODE_PERMISSION_VIDEO_IP_CALL) { if (CommonActivityUtils.onPermissionResultVideoIpCall(this, aPermissions, aGrantResults)) { - startIpCall(true); + startIpCall(PreferencesManager.useJitsiConfCall(this), true); } } else { Log.w(LOG_TAG, "## onRequestPermissionsResult(): Unknown requestCode =" + aRequestCode); @@ -2404,6 +2452,20 @@ public static String sanitizeDisplayname(String displayName) { return displayName; } + /** + * Insert a text in the text editor + * + * @param text the text + */ + public void insertTextInTextEditor(String text) { + // another user + if (TextUtils.isEmpty(mEditText.getText())) { + mEditText.append(text); + } else { + mEditText.getText().insert(mEditText.getSelectionStart(), text + " "); + } + } + /** * Insert an user displayname in the message editor. * @@ -2411,11 +2473,14 @@ public static String sanitizeDisplayname(String displayName) { */ public void insertUserDisplayNameInTextEditor(String text) { if (null != text) { + boolean vibrate = false; + if (TextUtils.equals(mSession.getMyUser().displayname, text)) { // current user if (TextUtils.isEmpty(mEditText.getText())) { - mEditText.setText(String.format("%s ", SlashComandsParser.CMD_EMOTE)); + mEditText.setText(String.format(VectorApp.getApplicationLocale(), "%s ", SlashComandsParser.CMD_EMOTE)); mEditText.setSelection(mEditText.getText().length()); + vibrate = true; } } else { // another user @@ -2424,6 +2489,15 @@ public void insertUserDisplayNameInTextEditor(String text) { } else { mEditText.getText().insert(mEditText.getSelectionStart(), sanitizeDisplayname(text) + " "); } + + vibrate = true; + } + + if (vibrate && PreferencesManager.vibrateWhenMentioning(this)) { + Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + if ((null != v) && v.hasVibrator()) { + v.vibrate(100); + } } } } @@ -2707,11 +2781,11 @@ private void onRoomTypings() { if (0 == names.size()) { mLatestTypingMessage = null; } else if (1 == names.size()) { - mLatestTypingMessage = String.format(this.getString(R.string.room_one_user_is_typing), names.get(0)); + mLatestTypingMessage = String.format(VectorApp.getApplicationLocale(), this.getString(R.string.room_one_user_is_typing), names.get(0)); } else if (2 == names.size()) { - mLatestTypingMessage = String.format(this.getString(R.string.room_two_users_are_typing), names.get(0), names.get(1)); + mLatestTypingMessage = String.format(VectorApp.getApplicationLocale(), this.getString(R.string.room_two_users_are_typing), names.get(0), names.get(1)); } else if (names.size() > 2) { - mLatestTypingMessage = String.format(this.getString(R.string.room_many_users_are_typing), names.get(0), names.get(1)); + mLatestTypingMessage = String.format(VectorApp.getApplicationLocale(), this.getString(R.string.room_many_users_are_typing), names.get(0), names.get(1)); } } @@ -3089,6 +3163,128 @@ private void enableActionBarHeader(boolean aIsHeaderViewDisplayed) { } } + //================================================================================ + // Kick / ban mode management + //================================================================================ + + /* + You have been kicked from %1$ by %2$ + Reason: %1$ + Rejoin + Forget room + */ + + /** + * Manage the room preview buttons area + */ + private void manageBannedHeader(RoomMember member) { + mRoomPreviewLayout.setVisibility(View.VISIBLE); + + TextView invitationTextView = findViewById(R.id.room_preview_invitation_textview); + + if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_BAN)) { + invitationTextView.setText(getString(R.string.has_been_banned, VectorUtils.getRoomDisplayName(this, mSession, mRoom), mRoom.getLiveState().getMemberName(member.mSender))); + } else { + invitationTextView.setText(getString(R.string.has_been_kicked, VectorUtils.getRoomDisplayName(this, mSession, mRoom), mRoom.getLiveState().getMemberName(member.mSender))); + } + + TextView subInvitationTextView = findViewById(R.id.room_preview_subinvitation_textview); + subInvitationTextView.setText(getString(R.string.reason_colon, member.reason)); + + + Button joinButton = findViewById(R.id.button_join_room); + + if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_BAN)) { + joinButton.setVisibility(View.INVISIBLE); + } else { + joinButton.setText(getString(R.string.rejoin)); + + joinButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setProgressVisibility(View.VISIBLE); + mSession.joinRoom(mRoom.getRoomId(), new ApiCallback() { + @Override + public void onSuccess(String roomId) { + setProgressVisibility(View.GONE); + + HashMap params = new HashMap<>(); + + params.put(VectorRoomActivity.EXTRA_MATRIX_ID, mSession.getMyUserId()); + params.put(VectorRoomActivity.EXTRA_ROOM_ID, mRoom.getRoomId()); + + // clear the activity stack to home activity + Intent intent = new Intent(VectorRoomActivity.this, VectorHomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + intent.putExtra(VectorHomeActivity.EXTRA_JUMP_TO_ROOM_PARAMS, params); + VectorRoomActivity.this.startActivity(intent); + } + + private void onError(String errorMessage) { + Log.d(LOG_TAG, "re join failed " + errorMessage); + CommonActivityUtils.displayToast(VectorRoomActivity.this, errorMessage); + setProgressVisibility(View.GONE); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }); + } + }); + } + + Button forgetRoomButton = findViewById(R.id.button_decline); + forgetRoomButton.setText(getString(R.string.forget_room)); + + forgetRoomButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mRoom.forget(new ApiCallback() { + @Override + public void onSuccess(Void info) { + VectorRoomActivity.this.finish(); + } + + private void onError(String errorMessage) { + Log.d(LOG_TAG, "forget failed " + errorMessage); + CommonActivityUtils.displayToast(VectorRoomActivity.this, errorMessage); + setProgressVisibility(View.GONE); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }); + } + }); + + enableActionBarHeader(SHOW_ACTION_BAR_HEADER); + } + //================================================================================ // Room preview management //================================================================================ diff --git a/vector/src/main/java/im/vector/activity/VectorRoomDetailsActivity.java b/vector/src/main/java/im/vector/activity/VectorRoomDetailsActivity.java index b1e56d5c2a..5c1b363122 100755 --- a/vector/src/main/java/im/vector/activity/VectorRoomDetailsActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorRoomDetailsActivity.java @@ -19,7 +19,6 @@ import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.FragmentTransaction; @@ -41,7 +40,6 @@ import im.vector.fragments.VectorRoomDetailsMembersFragment; import im.vector.fragments.VectorRoomSettingsFragment; import im.vector.fragments.VectorSearchRoomFilesListFragment; -import im.vector.util.ThemeUtils; /** * This class implements the room details screen, using a tab UI pattern. @@ -331,8 +329,6 @@ private void createNavigationTabs(Bundle aSavedInstanceState, int defaultSelecte tabIndexToRestore = PEOPLE_TAB_INDEX; } - mActionBar.setStackedBackgroundDrawable(new ColorDrawable(ThemeUtils.getColor(this, R.attr.tab_bar_background_color))); - // set the tab to display & set current tab index mActionBar.setSelectedNavigationItem(tabIndexToRestore); mCurrentTabIndex = tabIndexToRestore; diff --git a/vector/src/main/java/im/vector/activity/WidgetActivity.java b/vector/src/main/java/im/vector/activity/WidgetActivity.java index 8203888679..b50e813e5b 100755 --- a/vector/src/main/java/im/vector/activity/WidgetActivity.java +++ b/vector/src/main/java/im/vector/activity/WidgetActivity.java @@ -43,7 +43,6 @@ import butterknife.ButterKnife; import im.vector.Matrix; import im.vector.R; -import im.vector.VectorApp; import im.vector.widgets.Widget; import im.vector.widgets.WidgetsManager; diff --git a/vector/src/main/java/im/vector/adapters/AbsAdapter.java b/vector/src/main/java/im/vector/adapters/AbsAdapter.java index 546449c6ab..f42036d365 100644 --- a/vector/src/main/java/im/vector/adapters/AbsAdapter.java +++ b/vector/src/main/java/im/vector/adapters/AbsAdapter.java @@ -21,6 +21,7 @@ import android.support.v7.widget.RecyclerView; import android.text.TextUtils; +import org.matrix.androidsdk.rest.model.group.Group; import org.matrix.androidsdk.util.Log; import android.util.Pair; @@ -39,6 +40,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import im.vector.R; +import im.vector.util.GroupUtils; import im.vector.util.RoomUtils; import im.vector.util.StickySectionHelper; import im.vector.view.SectionView; @@ -52,6 +54,10 @@ public abstract class AbsAdapter extends AbsFilterableAdapter { static final int TYPE_ROOM = -3; + static final int TYPE_GROUP = -4; + + static final int TYPE_GROUP_INVITATION = -5; + // Helper handling the sticky view for each section private StickySectionHelper mStickySectionHelper; @@ -59,7 +65,7 @@ public abstract class AbsAdapter extends AbsFilterableAdapter { /// Ex <0, section 1 with 2 items>, <3, section 2> private final List> mSections; - private final AdapterSection mInviteSection; + private AdapterSection mInviteSection; /* * ********************************************************************************************* @@ -67,20 +73,33 @@ public abstract class AbsAdapter extends AbsFilterableAdapter { * ********************************************************************************************* */ - AbsAdapter(final Context context, final InvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { + AbsAdapter(final Context context, final RoomInvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { super(context, invitationListener, moreActionListener); registerAdapterDataObserver(new AdapterDataObserver()); mSections = new ArrayList<>(); - mInviteSection = new AdapterSection<>(context.getString(R.string.room_recents_invites), -1, R.layout.adapter_item_room_view, + mInviteSection = new AdapterSection<>(context, context.getString(R.string.room_recents_invites), -1, R.layout.adapter_item_room_view, TYPE_HEADER_DEFAULT, TYPE_ROOM_INVITATION, new ArrayList(), null); mInviteSection.setEmptyViewPlaceholder(null, context.getString(R.string.no_result_placeholder)); mInviteSection.setIsHiddenWhenEmpty(true); addSection(mInviteSection); } + + AbsAdapter(final Context context, final GroupInvitationListener invitationListener, final MoreGroupActionListener moreActionListener) { + super(context, invitationListener, moreActionListener); + registerAdapterDataObserver(new AdapterDataObserver()); + mSections = new ArrayList<>(); + } + + AbsAdapter(final Context context) { + super(context); + registerAdapterDataObserver(new AdapterDataObserver()); + mSections = new ArrayList<>(); + } + /* * ********************************************************************************************* * RecyclerView.Adapter methods @@ -122,7 +141,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewT return new HeaderViewHolder(headerItemView); case TYPE_ROOM_INVITATION: View invitationView = inflater.inflate(R.layout.adapter_item_room_invite, viewGroup, false); - return new InvitationViewHolder(invitationView); + return new RoomInvitationViewHolder(invitationView); default: return createSubViewHolder(viewGroup, viewType); } @@ -157,9 +176,9 @@ public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, int posit } break; case TYPE_ROOM_INVITATION: - final InvitationViewHolder invitationViewHolder = (InvitationViewHolder) viewHolder; + final RoomInvitationViewHolder invitationViewHolder = (RoomInvitationViewHolder) viewHolder; final Room room = (Room) getItemForPosition(position); - invitationViewHolder.populateViews(mContext, mSession, room, mInvitationListener, mMoreActionListener); + invitationViewHolder.populateViews(mContext, mSession, room, mRoomInvitationListener, mMoreRoomActionListener); break; default: populateViewHolder(viewType, viewHolder, position); @@ -207,12 +226,14 @@ protected void publishResults(CharSequence constraint, FilterResults results) { * @param rooms */ public void setInvitation(final List rooms) { - mInviteSection.setItems(rooms, mCurrentFilterPattern); - if (!TextUtils.isEmpty(mCurrentFilterPattern)) { - filterRoomSection(mInviteSection, String.valueOf(mCurrentFilterPattern)); - } + if (null != mInviteSection) { + mInviteSection.setItems(rooms, mCurrentFilterPattern); + if (!TextUtils.isEmpty(mCurrentFilterPattern)) { + filterRoomSection(mInviteSection, String.valueOf(mCurrentFilterPattern)); + } - updateSections(); + updateSections(); + } } /** @@ -353,13 +374,38 @@ int getSectionHeaderPosition(final AdapterSection section) { * @return nb of items matching the filter */ int filterRoomSection(final AdapterSection section, final String filterPattern) { - if (!TextUtils.isEmpty(filterPattern)) { - List filteredRoom = RoomUtils.getFilteredRooms(mContext, mSession, section.getItems(), filterPattern); - section.setFilteredItems(filteredRoom, filterPattern); + if (null != section) { + if (!TextUtils.isEmpty(filterPattern)) { + List filteredRoom = RoomUtils.getFilteredRooms(mContext, mSession, section.getItems(), filterPattern); + section.setFilteredItems(filteredRoom, filterPattern); + } else { + section.resetFilter(); + } + return section.getFilteredItems().size(); } else { - section.resetFilter(); + return 0; + } + } + + /** + * Filter the given section of groups with the given pattern + * + * @param section + * @param filterPattern + * @return nb of items matching the filter + */ + int filterGroupSection(final AdapterSection section, final String filterPattern) { + if (null != section) { + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroups = GroupUtils.getFilteredGroups(section.getItems(), filterPattern); + section.setFilteredItems(filteredGroups, filterPattern); + } else { + section.resetFilter(); + } + return section.getFilteredItems().size(); + } else { + return 0; } - return section.getFilteredItems().size(); } /* @@ -430,16 +476,26 @@ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { } } - public interface InvitationListener { + public interface RoomInvitationListener { void onPreviewRoom(MXSession session, String roomId); void onRejectInvitation(MXSession session, String roomId); } + public interface GroupInvitationListener { + void onJoinGroup(MXSession session, String groupId); + + void onRejectInvitation(MXSession session, String groupId); + } + public interface MoreRoomActionListener { void onMoreActionClick(View itemView, Room room); } + public interface MoreGroupActionListener { + void onMoreActionClick(View itemView, Group group); + } + /* * ********************************************************************************************* * Abstract methods diff --git a/vector/src/main/java/im/vector/adapters/AbsFilterableAdapter.java b/vector/src/main/java/im/vector/adapters/AbsFilterableAdapter.java index 52bbb03721..0f26a43ea7 100644 --- a/vector/src/main/java/im/vector/adapters/AbsFilterableAdapter.java +++ b/vector/src/main/java/im/vector/adapters/AbsFilterableAdapter.java @@ -38,21 +38,40 @@ public abstract class AbsFilterableAdapter ex CharSequence mCurrentFilterPattern; private final Filter mFilter; - final AbsAdapter.InvitationListener mInvitationListener; - final AbsAdapter.MoreRoomActionListener mMoreActionListener; - + AbsAdapter.RoomInvitationListener mRoomInvitationListener; + AbsAdapter.GroupInvitationListener mGroupInvitationListener; + AbsAdapter.MoreRoomActionListener mMoreRoomActionListener; + AbsAdapter.MoreGroupActionListener mMoreGroupActionListener; /* * ********************************************************************************************* * Constructor * ********************************************************************************************* */ - AbsFilterableAdapter(final Context context, final AbsAdapter.InvitationListener invitationListener, + AbsFilterableAdapter(final Context context) { + mContext = context; + + mSession = Matrix.getInstance(context).getDefaultSession(); + mFilter = createFilter(); + } + + AbsFilterableAdapter(final Context context, final AbsAdapter.RoomInvitationListener invitationListener, final AbsAdapter.MoreRoomActionListener moreActionListener) { mContext = context; - mInvitationListener = invitationListener; - mMoreActionListener = moreActionListener; + mRoomInvitationListener = invitationListener; + mMoreRoomActionListener = moreActionListener; + + mSession = Matrix.getInstance(context).getDefaultSession(); + mFilter = createFilter(); + } + + AbsFilterableAdapter(final Context context, final AbsAdapter.GroupInvitationListener invitationListener, + final AbsAdapter.MoreGroupActionListener moreActionListener) { + mContext = context; + + mGroupInvitationListener = invitationListener; + mMoreGroupActionListener = moreActionListener; mSession = Matrix.getInstance(context).getDefaultSession(); mFilter = createFilter(); diff --git a/vector/src/main/java/im/vector/adapters/AdapterSection.java b/vector/src/main/java/im/vector/adapters/AdapterSection.java index 686b866b8b..53a37c5639 100644 --- a/vector/src/main/java/im/vector/adapters/AdapterSection.java +++ b/vector/src/main/java/im/vector/adapters/AdapterSection.java @@ -16,6 +16,7 @@ package im.vector.adapters; +import android.content.Context; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; @@ -53,8 +54,11 @@ public class AdapterSection { private boolean mIsHiddenWhenEmpty; private boolean mIsHiddenWhenNoFilter; - public AdapterSection(String title, int headerSubViewResId, int contentResId, int headerViewType, + private Context mContext; + + public AdapterSection(Context context , String title, int headerSubViewResId, int contentResId, int headerViewType, int contentViewType, List items, Comparator comparator) { + mContext = context; mTitle = title; mItems = items; mFilteredItems = new ArrayList<>(items); @@ -117,8 +121,8 @@ void updateTitle() { * @param titleToFormat */ void formatTitle(final String titleToFormat) { - SpannableString spannableString = new SpannableString(titleToFormat.toUpperCase()); - spannableString.setSpan(new ForegroundColorSpan(ThemeUtils.getColor(VectorApp.getInstance(), R.attr.list_header_subtext_color)), + SpannableString spannableString = new SpannableString(titleToFormat.toUpperCase(VectorApp.getApplicationLocale())); + spannableString.setSpan(new ForegroundColorSpan(ThemeUtils.getColor(mContext, R.attr.list_header_subtext_color)), mTitle.length(), titleToFormat.length(), 0); mTitleFormatted = spannableString; } diff --git a/vector/src/main/java/im/vector/adapters/AutoCompletedUserAdapter.java b/vector/src/main/java/im/vector/adapters/AutoCompletedUserAdapter.java index 2a4d3a60e3..ba7900d014 100755 --- a/vector/src/main/java/im/vector/adapters/AutoCompletedUserAdapter.java +++ b/vector/src/main/java/im/vector/adapters/AutoCompletedUserAdapter.java @@ -27,6 +27,7 @@ import org.matrix.androidsdk.MXSession; import im.vector.R; +import im.vector.VectorApp; import im.vector.activity.VectorRoomActivity; import im.vector.util.VectorUtils; import im.vector.view.VectorCircularImageView; @@ -179,18 +180,18 @@ protected FilterResults performFiltering(CharSequence prefix) { mIsSearchingMatrixId = true; } else { newValues = new ArrayList<>(); - String prefixString = prefix.toString().toLowerCase(); + String prefixString = prefix.toString().toLowerCase(VectorApp.getApplicationLocale()); mIsSearchingMatrixId = prefixString.startsWith("@"); if (mIsSearchingMatrixId) { for (User user : mUsersList) { - if ((null != user.user_id) && user.user_id.toLowerCase().startsWith(prefixString)) { + if ((null != user.user_id) && user.user_id.toLowerCase(VectorApp.getApplicationLocale()).startsWith(prefixString)) { newValues.add(user); } } } else { for (User user : mUsersList) { - if ((null != user.displayname) && user.displayname.toLowerCase().startsWith(prefixString)) { + if ((null != user.displayname) && user.displayname.toLowerCase(VectorApp.getApplicationLocale()).startsWith(prefixString)) { newValues.add(user); } } diff --git a/vector/src/main/java/im/vector/adapters/GroupAdapter.java b/vector/src/main/java/im/vector/adapters/GroupAdapter.java new file mode 100644 index 0000000000..ddf5bf37b5 --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupAdapter.java @@ -0,0 +1,171 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import im.vector.R; + +public class GroupAdapter extends AbsAdapter { + private static final String LOG_TAG = GroupAdapter.class.getSimpleName(); + + private final AdapterSection mInvitedGroupsSection; + private final AdapterSection mGroupsSection; + + private final OnGroupSelectItemListener mListener; + + /* + * ********************************************************************************************* + * Constructor + * ********************************************************************************************* + */ + + public GroupAdapter(final Context context, final OnGroupSelectItemListener listener, final GroupInvitationListener invitationListener, final MoreGroupActionListener moreActionListener) { + super(context, invitationListener, moreActionListener); + + mListener = listener; + + mInvitedGroupsSection = new AdapterSection<>(context, context.getString(R.string.groups_invite_header), -1, + R.layout.adapter_item_group_invite, TYPE_HEADER_DEFAULT, TYPE_GROUP_INVITATION, new ArrayList(), Group.mGroupsComparator); + mInvitedGroupsSection.setEmptyViewPlaceholder(context.getString(R.string.no_group_placeholder), context.getString(R.string.no_result_placeholder)); + mInvitedGroupsSection.setIsHiddenWhenEmpty(true); + + mGroupsSection = new AdapterSection<>(context, context.getString(R.string.groups_header), -1, + R.layout.adapter_item_group_view, TYPE_HEADER_DEFAULT, TYPE_GROUP, new ArrayList(), Group.mGroupsComparator); + mGroupsSection.setEmptyViewPlaceholder(context.getString(R.string.no_group_placeholder), context.getString(R.string.no_result_placeholder)); + + addSection(mInvitedGroupsSection); + addSection(mGroupsSection); + } + + /* + * ********************************************************************************************* + * Abstract methods implementation + * ********************************************************************************************* + */ + + @Override + protected RecyclerView.ViewHolder createSubViewHolder(ViewGroup viewGroup, int viewType) { + Log.i(LOG_TAG, " onCreateViewHolder for viewType:" + viewType); + final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + + switch (viewType) { + case TYPE_GROUP: + return new GroupViewHolder(inflater.inflate(R.layout.adapter_item_group_view, viewGroup, false)); + case TYPE_GROUP_INVITATION: + return new GroupInvitationViewHolder(inflater.inflate(R.layout.adapter_item_group_invite, viewGroup, false)); + } + + return null; + } + + @Override + protected void populateViewHolder(int viewType, RecyclerView.ViewHolder viewHolder, int position) { + View groupView = null; + Group group = null; + + switch (viewType) { + case TYPE_GROUP: { + final GroupViewHolder groupViewHolder = (GroupViewHolder) viewHolder; + group = (Group) getItemForPosition(position); + groupViewHolder.populateViews(mContext, mSession, group, null, false, mMoreGroupActionListener); + groupView = groupViewHolder.itemView; + + break; + } + case TYPE_GROUP_INVITATION: { + final GroupInvitationViewHolder groupViewHolder = (GroupInvitationViewHolder) viewHolder; + group = (Group) getItemForPosition(position); + groupViewHolder.populateViews(mContext, mSession, group, mGroupInvitationListener, true, mMoreGroupActionListener); + groupView = groupViewHolder.itemView; + break; + } + } + + if (null != groupView) { + final Group fGroup = group; + + groupView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onSelectItem(fGroup, -1); + } + }); + + groupView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return mListener.onLongPressItem(fGroup, -1); + } + }); + } + } + + @Override + protected int applyFilter(String pattern) { + int nbResults = 0; + + nbResults += filterGroupSection(mInvitedGroupsSection, pattern); + nbResults += filterGroupSection(mGroupsSection, pattern); + + return nbResults; + } + + /* + * ********************************************************************************************* + * Public methods + * ********************************************************************************************* + */ + + public void setGroups(final List groups) { + mGroupsSection.setItems(groups, mCurrentFilterPattern); + if (!TextUtils.isEmpty(mCurrentFilterPattern)) { + filterGroupSection(mGroupsSection, String.valueOf(mCurrentFilterPattern)); + } + updateSections(); + } + + public void setInvitedGroups(final List groups) { + mInvitedGroupsSection.setItems(groups, mCurrentFilterPattern); + if (!TextUtils.isEmpty(mCurrentFilterPattern)) { + filterGroupSection(mInvitedGroupsSection, String.valueOf(mCurrentFilterPattern)); + } + updateSections(); + } + + /* + * ********************************************************************************************* + * Inner classes + * ********************************************************************************************* + */ + + public interface OnGroupSelectItemListener { + void onSelectItem(Group item, int position); + boolean onLongPressItem(Group item, int position); + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupAdapterSection.java b/vector/src/main/java/im/vector/adapters/GroupAdapterSection.java new file mode 100644 index 0000000000..0d8169b8ff --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupAdapterSection.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; + +import java.util.Comparator; +import java.util.List; + +public class GroupAdapterSection extends AdapterSection { + + public GroupAdapterSection(Context context , String title, int headerSubViewResId, int contentResId, int headerViewType, + int contentViewType, List items, Comparator comparator) { + super(context, title, headerSubViewResId, contentResId, headerViewType, contentViewType, items, comparator); + } + + /** + * Update the title depending on the number of items + */ + void updateTitle() { + String newTitle; + + // the group members / rooms lists are estimated + // it seems safer to display the count only for the filtered lists + if ((getItems().size() != getFilteredItems().size()) && (getNbItems() > 0)) { + newTitle = mTitle.concat(" " + getNbItems()); + } else { + newTitle = mTitle; + } + + formatTitle(newTitle); + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupDetailsFragmentPagerAdapter.java b/vector/src/main/java/im/vector/adapters/GroupDetailsFragmentPagerAdapter.java new file mode 100755 index 0000000000..5d3a3a5c89 --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupDetailsFragmentPagerAdapter.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.view.ViewGroup; + +import im.vector.R; +import im.vector.fragments.GroupDetailsBaseFragment; +import im.vector.fragments.GroupDetailsHomeFragment; +import im.vector.fragments.GroupDetailsPeopleFragment; +import im.vector.fragments.GroupDetailsRoomsFragment; + +/** + * Groups pager adapter + */ +public class GroupDetailsFragmentPagerAdapter extends FragmentPagerAdapter { + private static final String LOG_TAG = GroupDetailsFragmentPagerAdapter.class.getSimpleName(); + + private static final int HOME_FRAGMENT_INDEX = 0; + private static final int PEOPLE_FRAGMENT_INDEX = 1; + private static final int ROOMS_FRAGMENT_INDEX = 2; + private static final int FRAGMENTS_COUNT = 3; + + private final Context mContext; + + private GroupDetailsHomeFragment mHomeFragment; + private GroupDetailsPeopleFragment mPeopleFragment; + private GroupDetailsRoomsFragment mRoomsFragment; + + public GroupDetailsFragmentPagerAdapter(FragmentManager fm, Context context) { + super(fm); + mContext = context; + } + + @Override + public int getCount() { + return FRAGMENTS_COUNT; + } + + @Override + public Fragment getItem(int position) { + Fragment fragment; + + switch (position) { + case HOME_FRAGMENT_INDEX: { + fragment = mHomeFragment; + + if (null == fragment) { + fragment = mHomeFragment = new GroupDetailsHomeFragment(); + } + break; + } + case PEOPLE_FRAGMENT_INDEX: { + fragment = mPeopleFragment; + + if (null == fragment) { + fragment = mPeopleFragment = new GroupDetailsPeopleFragment(); + } + break; + } + case ROOMS_FRAGMENT_INDEX: { + fragment = mRoomsFragment; + + if (null == fragment) { + fragment = mRoomsFragment = new GroupDetailsRoomsFragment(); + } + break; + } + default: + fragment = null; + } + + return fragment; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case HOME_FRAGMENT_INDEX: { + return mContext.getString(R.string.group_details_home); + } + case PEOPLE_FRAGMENT_INDEX: { + return mContext.getString(R.string.group_details_people); + } + case ROOMS_FRAGMENT_INDEX: { + return mContext.getString(R.string.group_details_rooms); + } + } + + return super.getPageTitle(position); + } + + /** + * @return the home fragment + */ + public GroupDetailsBaseFragment getHomeFragment() { + return mHomeFragment; + } + + /** + * @return the people fragment + */ + public GroupDetailsBaseFragment getPeopleFragment() { + return mPeopleFragment; + } + + /** + * @return the rooms fragment + */ + public GroupDetailsBaseFragment getRoomsFragment() { + return mRoomsFragment; + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupDetailsPeopleAdapter.java b/vector/src/main/java/im/vector/adapters/GroupDetailsPeopleAdapter.java new file mode 100644 index 0000000000..10f113a4be --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupDetailsPeopleAdapter.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.matrix.androidsdk.rest.model.group.GroupUser; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import im.vector.R; +import im.vector.util.GroupUtils; + +public class GroupDetailsPeopleAdapter extends AbsAdapter { + private static final int TYPE_JOINED_USERS = 12; + private static final int TYPE_INVITED_USERS = 13; + + private final GroupAdapterSection mJoinedUsersSection; + private final GroupAdapterSection mInvitedUsersSection; + + private final OnSelectUserListener mListener; + + private static final Comparator mComparator = new Comparator() { + @Override + public int compare(GroupUser lhs, GroupUser rhs) { + return lhs.getDisplayname().compareTo(rhs.getDisplayname()); + } + }; + + /* + * ********************************************************************************************* + * Constructor + * ********************************************************************************************* + */ + + public GroupDetailsPeopleAdapter(final Context context, final OnSelectUserListener listener) { + super(context); + + mListener = listener; + + mJoinedUsersSection = new GroupAdapterSection<>(context, context.getString(R.string.joined), -1, + R.layout.adapter_item_group_user_room_view, TYPE_HEADER_DEFAULT, TYPE_JOINED_USERS, new ArrayList(), mComparator); + mJoinedUsersSection.setEmptyViewPlaceholder(context.getString(R.string.no_users_placeholder), context.getString(R.string.no_result_placeholder)); + + mInvitedUsersSection = new GroupAdapterSection<>(context, context.getString(R.string.invited), -1, + R.layout.adapter_item_group_user_room_view, TYPE_HEADER_DEFAULT, TYPE_INVITED_USERS, new ArrayList(), mComparator); + mInvitedUsersSection.setEmptyViewPlaceholder(context.getString(R.string.no_users_placeholder), context.getString(R.string.no_result_placeholder)); + mInvitedUsersSection.setIsHiddenWhenEmpty(true); + + addSection(mJoinedUsersSection); + addSection(mInvitedUsersSection); + } + + /* + * ********************************************************************************************* + * Abstract methods implementation + * ********************************************************************************************* + */ + + @Override + protected RecyclerView.ViewHolder createSubViewHolder(ViewGroup viewGroup, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + + if ((viewType == TYPE_JOINED_USERS) || (viewType == TYPE_INVITED_USERS)) { + return new GroupUserViewHolder(inflater.inflate(R.layout.adapter_item_group_user_room_view, viewGroup, false)); + } + return null; + } + + @Override + protected void populateViewHolder(int viewType, RecyclerView.ViewHolder viewHolder, int position) { + switch (viewType) { + case TYPE_JOINED_USERS: + case TYPE_INVITED_USERS: + final GroupUserViewHolder groupUserViewHolder = (GroupUserViewHolder) viewHolder; + final GroupUser groupUser = (GroupUser) getItemForPosition(position); + groupUserViewHolder.populateViews(mContext, mSession, groupUser); + + groupUserViewHolder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onSelectItem(groupUser, -1); + } + }); + break; + } + } + + /** + * Filter the given section of rooms with the given pattern + * + * @param section + * @param filterPattern + * @return nb of items matching the filter + */ + int filterGroupUsersSection(final AdapterSection section, final String filterPattern) { + if (null != section) { + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroupUsers = GroupUtils.getFilteredGroupUsers(section.getItems(), filterPattern); + section.setFilteredItems(filteredGroupUsers, filterPattern); + } else { + section.resetFilter(); + } + return section.getFilteredItems().size(); + } else { + return 0; + } + } + + + @Override + protected int applyFilter(String pattern) { + int nbResults = 0; + + nbResults += filterGroupUsersSection(mJoinedUsersSection, pattern); + nbResults += filterGroupUsersSection(mInvitedUsersSection, pattern); + + return nbResults; + } + + /* + * ********************************************************************************************* + * Public methods + * ********************************************************************************************* + */ + + public void setJoinedGroupUsers(final List users) { + mJoinedUsersSection.setItems(users, mCurrentFilterPattern); + if (!TextUtils.isEmpty(mCurrentFilterPattern)) { + filterGroupUsersSection(mJoinedUsersSection, String.valueOf(mCurrentFilterPattern)); + } + updateSections(); + } + + public void setInvitedGroupUsers(final List users) { + mInvitedUsersSection.setItems(users, mCurrentFilterPattern); + if (!TextUtils.isEmpty(mCurrentFilterPattern)) { + filterGroupUsersSection(mInvitedUsersSection, String.valueOf(mCurrentFilterPattern)); + } + updateSections(); + } + + /* + * ********************************************************************************************* + * Inner classes + * ********************************************************************************************* + */ + + public interface OnSelectUserListener { + void onSelectItem(GroupUser user, int position); + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupDetailsRoomsAdapter.java b/vector/src/main/java/im/vector/adapters/GroupDetailsRoomsAdapter.java new file mode 100644 index 0000000000..afba660f91 --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupDetailsRoomsAdapter.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.matrix.androidsdk.rest.model.group.GroupRoom; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import im.vector.R; +import im.vector.util.GroupUtils; + +public class GroupDetailsRoomsAdapter extends AbsAdapter { + private static final int TYPE_GROUP_ROOMS = 22; + + private static final Comparator mComparator = new Comparator() { + @Override + public int compare(GroupRoom lhs, GroupRoom rhs) { + return lhs.getDisplayName().compareTo(rhs.getDisplayName()); + } + }; + + private final GroupAdapterSection mGroupRoomsSection; + private final OnSelectRoomListener mListener; + + /* + * ********************************************************************************************* + * Constructor + * ********************************************************************************************* + */ + + public GroupDetailsRoomsAdapter(final Context context, final OnSelectRoomListener listener) { + super(context); + mListener = listener; + + mGroupRoomsSection = new GroupAdapterSection<>(context, context.getString(R.string.rooms), -1, + R.layout.adapter_item_group_user_room_view, TYPE_HEADER_DEFAULT, TYPE_GROUP_ROOMS, new ArrayList(), mComparator); + mGroupRoomsSection.setEmptyViewPlaceholder(null, context.getString(R.string.no_result_placeholder)); + + addSection(mGroupRoomsSection); + } + + /* + * ********************************************************************************************* + * Abstract methods implementation + * ********************************************************************************************* + */ + + @Override + protected RecyclerView.ViewHolder createSubViewHolder(ViewGroup viewGroup, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + + if (viewType == TYPE_GROUP_ROOMS) { + return new GroupRoomViewHolder(inflater.inflate(R.layout.adapter_item_group_user_room_view, viewGroup, false)); + } + return null; + } + + @Override + protected void populateViewHolder(int viewType, RecyclerView.ViewHolder viewHolder, int position) { + switch (viewType) { + case TYPE_GROUP_ROOMS: + final GroupRoomViewHolder groupRoomViewHolder = (GroupRoomViewHolder) viewHolder; + final GroupRoom groupRoom = (GroupRoom) getItemForPosition(position); + groupRoomViewHolder.populateViews(mContext, mSession, groupRoom); + + groupRoomViewHolder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onSelectItem(groupRoom, -1); + } + }); + break; + } + } + + /** + * Filter the given section of rooms with the given pattern + * + * @param section + * @param filterPattern + * @return nb of items matching the filter + */ + int filterGroupRoomsSection(final AdapterSection section, final String filterPattern) { + if (null != section) { + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroupRooms = GroupUtils.getFilteredGroupRooms(section.getItems(), filterPattern); + section.setFilteredItems(filteredGroupRooms, filterPattern); + } else { + section.resetFilter(); + } + return section.getFilteredItems().size(); + } else { + return 0; + } + } + + + @Override + protected int applyFilter(String pattern) { + return filterGroupRoomsSection(mGroupRoomsSection, pattern); + } + + /* + * ********************************************************************************************* + * Public methods + * ********************************************************************************************* + */ + + public void setGroupRooms(final List rooms) { + mGroupRoomsSection.setItems(rooms, mCurrentFilterPattern); + updateSections(); + } + + /* + * ********************************************************************************************* + * Inner classes + * ********************************************************************************************* + */ + + public interface OnSelectRoomListener { + void onSelectItem(GroupRoom groupRoom, int position); + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupInvitationViewHolder.java b/vector/src/main/java/im/vector/adapters/GroupInvitationViewHolder.java new file mode 100644 index 0000000000..4521835ceb --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupInvitationViewHolder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.view.View; +import android.widget.Button; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.model.group.Group; + +import butterknife.BindView; +import im.vector.R; + +public class GroupInvitationViewHolder extends GroupViewHolder { + + @BindView(R.id.group_invite_reject_button) + Button vRejectButton; + + @BindView(R.id.group_invite_join_button) + Button vJoinButton; + + GroupInvitationViewHolder(View itemView) { + super(itemView); + } + + @Override + public void populateViews(final Context context, final MXSession session, final Group group, final AbsAdapter.GroupInvitationListener invitationListener, final boolean isInvitation, + final AbsAdapter.MoreGroupActionListener moreGroupActionListener) { + super.populateViews(context, session, group, invitationListener, true, moreGroupActionListener); + + vJoinButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != invitationListener) { + invitationListener.onJoinGroup(session, group.getGroupId()); + } + } + }); + + vRejectButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != invitationListener) { + invitationListener.onRejectInvitation(session, group.getGroupId()); + } + } + }); + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/adapters/GroupRoomViewHolder.java b/vector/src/main/java/im/vector/adapters/GroupRoomViewHolder.java new file mode 100644 index 0000000000..bd35f78d2f --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupRoomViewHolder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.model.group.GroupRoom; +import org.matrix.androidsdk.util.Log; + +import butterknife.BindView; +import butterknife.ButterKnife; +import im.vector.R; +import im.vector.util.VectorUtils; + +public class GroupRoomViewHolder extends RecyclerView.ViewHolder { + private static final String LOG_TAG = GroupRoomViewHolder.class.getSimpleName(); + + @BindView(R.id.contact_avatar) + ImageView vContactAvatar; + + @BindView(R.id.contact_name) + TextView vContactName; + + @Nullable + @BindView(R.id.contact_desc) + TextView vContactDesc; + + public GroupRoomViewHolder(final View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + /** + * Refresh the holder layout + * + * @param context the context + * @param session the session + * @param groupRoom the group room + */ + public void populateViews(final Context context, final MXSession session, final GroupRoom groupRoom) { + // sanity check + if (null == groupRoom) { + Log.e(LOG_TAG, "## populateViews() : null groupRoom"); + return; + } + + if (null == session) { + Log.e(LOG_TAG, "## populateViews() : null session"); + return; + } + + if (null == session.getDataHandler()) { + Log.e(LOG_TAG, "## populateViews() : null dataHandler"); + return; + } + + vContactName.setText(groupRoom.getDisplayName()); + VectorUtils.loadUserAvatar(context, session, vContactAvatar, groupRoom.avatar_url, groupRoom.roomId, groupRoom.getDisplayName()); + + if (null != vContactDesc) { + vContactDesc.setText(groupRoom.topic); + } + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupUserViewHolder.java b/vector/src/main/java/im/vector/adapters/GroupUserViewHolder.java new file mode 100644 index 0000000000..7c737c2f72 --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupUserViewHolder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.model.group.GroupUser; +import org.matrix.androidsdk.util.Log; + +import butterknife.BindView; +import butterknife.ButterKnife; +import im.vector.R; +import im.vector.util.VectorUtils; + +public class GroupUserViewHolder extends RecyclerView.ViewHolder { + private static final String LOG_TAG = GroupUserViewHolder.class.getSimpleName(); + + @BindView(R.id.contact_avatar) + ImageView vContactAvatar; + + @BindView(R.id.contact_name) + TextView vContactName; + + public GroupUserViewHolder(final View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + /** + * Refresh the holder layout + * + * @param context the context + * @param session the session + * @param groupUser the user + */ + public void populateViews(final Context context, final MXSession session, final GroupUser groupUser) { + // sanity check + if (null == groupUser) { + Log.e(LOG_TAG, "## populateViews() : null groupUser"); + return; + } + + if (null == session) { + Log.e(LOG_TAG, "## populateViews() : null session"); + return; + } + + if (null == session.getDataHandler()) { + Log.e(LOG_TAG, "## populateViews() : null dataHandler"); + return; + } + + vContactName.setText(groupUser.getDisplayname()); + VectorUtils.loadUserAvatar(context, session, vContactAvatar, groupUser.avatarUrl, groupUser.userId, groupUser.getDisplayname()); + } +} diff --git a/vector/src/main/java/im/vector/adapters/GroupViewHolder.java b/vector/src/main/java/im/vector/adapters/GroupViewHolder.java new file mode 100644 index 0000000000..204687d239 --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/GroupViewHolder.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.Context; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.util.Log; + +import butterknife.BindView; +import butterknife.ButterKnife; +import im.vector.R; +import im.vector.util.VectorUtils; + +public class GroupViewHolder extends RecyclerView.ViewHolder { + private static final String LOG_TAG = GroupViewHolder.class.getSimpleName(); + + @BindView(R.id.room_avatar) + ImageView vGroupAvatar; + + @BindView(R.id.group_name) + TextView vGroupName; + + @BindView(R.id.group_topic) + @Nullable + TextView vGroupTopic; + + @BindView(R.id.group_members_count) + TextView vGroupMembersCount; + + @BindView(R.id.group_more_action_click_area) + @Nullable + View vGroupMoreActionClickArea; + + @BindView(R.id.group_more_action_anchor) + @Nullable + View vGroupMoreActionAnchor; + + public GroupViewHolder(final View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + /** + * Refresh the holder layout + * + * @param context the context + * @param group the group + * @param isInvitation true if it is an invitation + * @param moreGroupActionListener the more actions listener + */ + public void populateViews(final Context context, final MXSession session, final Group group, final AbsAdapter.GroupInvitationListener invitationListener, final boolean isInvitation, + final AbsAdapter.MoreGroupActionListener moreGroupActionListener) { + // sanity check + if (null == group) { + Log.e(LOG_TAG, "## populateViews() : null group"); + return; + } + + if (isInvitation) { + vGroupMembersCount.setText("!"); + vGroupMembersCount.setTypeface(null, Typeface.BOLD); + GradientDrawable shape = new GradientDrawable(); + shape.setShape(GradientDrawable.RECTANGLE); + shape.setCornerRadius(100); + shape.setColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)); + vGroupMembersCount.setBackground(shape); + vGroupMembersCount.setVisibility(View.VISIBLE); + } else { + vGroupMembersCount.setVisibility(View.GONE); + } + + vGroupName.setText(group.getDisplayName()); + vGroupName.setTypeface(null, Typeface.NORMAL); + + VectorUtils.loadGroupAvatar(context, session, vGroupAvatar, group); + + vGroupTopic.setText(group.getShortDescription()); + + if (vGroupMoreActionClickArea != null && vGroupMoreActionAnchor != null) { + vGroupMoreActionClickArea.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != moreGroupActionListener) { + moreGroupActionListener.onMoreActionClick(vGroupMoreActionAnchor, group); + } + } + }); + } + } +} diff --git a/vector/src/main/java/im/vector/adapters/HomeRoomAdapter.java b/vector/src/main/java/im/vector/adapters/HomeRoomAdapter.java index a28c892321..01805e2dba 100644 --- a/vector/src/main/java/im/vector/adapters/HomeRoomAdapter.java +++ b/vector/src/main/java/im/vector/adapters/HomeRoomAdapter.java @@ -47,7 +47,7 @@ public class HomeRoomAdapter extends AbsFilterableAdapter { */ public HomeRoomAdapter(final Context context, @LayoutRes final int layoutRes, final OnSelectRoomListener listener, - final AbsAdapter.InvitationListener invitationListener, final AbsAdapter.MoreRoomActionListener moreActionListener) { + final AbsAdapter.RoomInvitationListener invitationListener, final AbsAdapter.MoreRoomActionListener moreActionListener) { super(context, invitationListener, moreActionListener); mRooms = new ArrayList<>(); @@ -68,7 +68,7 @@ public HomeRoomAdapter(final Context context, @LayoutRes final int layoutRes, fi public RoomViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { final LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext()); final View view = layoutInflater.inflate(mLayoutRes, viewGroup, false); - return mLayoutRes == R.layout.adapter_item_room_invite ? new InvitationViewHolder(view) : new RoomViewHolder(view); + return mLayoutRes == R.layout.adapter_item_room_invite ? new RoomInvitationViewHolder(view) : new RoomViewHolder(view); } @Override @@ -77,8 +77,8 @@ public void onBindViewHolder(final RoomViewHolder viewHolder, int position) { if (position < mFilteredRooms.size()) { final Room room = mFilteredRooms.get(position); if (mLayoutRes == R.layout.adapter_item_room_invite) { - final InvitationViewHolder invitationViewHolder = (InvitationViewHolder) viewHolder; - invitationViewHolder.populateViews(mContext, mSession, room, mInvitationListener, mMoreActionListener); + final RoomInvitationViewHolder invitationViewHolder = (RoomInvitationViewHolder) viewHolder; + invitationViewHolder.populateViews(mContext, mSession, room, mRoomInvitationListener, mMoreActionListener); } else { viewHolder.populateViews(mContext, mSession, room, mSession.getDirectChatRoomIdsList().contains(room.getRoomId()), false, mMoreActionListener); viewHolder.itemView.setOnClickListener(new View.OnClickListener() { diff --git a/vector/src/main/java/im/vector/adapters/KnownContactsAdapterSection.java b/vector/src/main/java/im/vector/adapters/KnownContactsAdapterSection.java index ab8cd0ad20..7716ad3174 100644 --- a/vector/src/main/java/im/vector/adapters/KnownContactsAdapterSection.java +++ b/vector/src/main/java/im/vector/adapters/KnownContactsAdapterSection.java @@ -16,6 +16,7 @@ package im.vector.adapters; +import android.content.Context; import android.text.TextUtils; import java.util.Comparator; @@ -27,9 +28,9 @@ class KnownContactsAdapterSection extends AdapterSection private boolean mIsLimited; private String mCustomHeaderExtra; - public KnownContactsAdapterSection(String title, int headerSubViewResId, int contentResId, int headerViewType, + public KnownContactsAdapterSection(Context context, String title, int headerSubViewResId, int contentResId, int headerViewType, int contentViewType, List items, Comparator comparator) { - super(title, headerSubViewResId, contentResId, headerViewType, contentViewType, items, comparator); + super(context, title, headerSubViewResId, contentResId, headerViewType, contentViewType, items, comparator); } /** diff --git a/vector/src/main/java/im/vector/adapters/ParticipantAdapterItem.java b/vector/src/main/java/im/vector/adapters/ParticipantAdapterItem.java index 4092e65f35..1a1e59f3f4 100755 --- a/vector/src/main/java/im/vector/adapters/ParticipantAdapterItem.java +++ b/vector/src/main/java/im/vector/adapters/ParticipantAdapterItem.java @@ -136,11 +136,11 @@ public ParticipantAdapterItem(String displayName, String avatarUrl, String userI */ private void initSearchByPatternFields() { if (!TextUtils.isEmpty(mDisplayName)) { - mLowerCaseDisplayName = mDisplayName.toLowerCase(); + mLowerCaseDisplayName = mDisplayName.toLowerCase(VectorApp.getApplicationLocale()); } if (!TextUtils.isEmpty(mUserId)) { - mLowerCaseMatrixId = mUserId.toLowerCase(); + mLowerCaseMatrixId = mUserId.toLowerCase(VectorApp.getApplicationLocale()); } } @@ -352,7 +352,7 @@ public boolean startsWith(String prefix) { if (componentsArrays.length > 0) { for (int i = 0; i < componentsArrays.length; i++) { - mDisplayNameComponents.add(componentsArrays[i].trim().toLowerCase()); + mDisplayNameComponents.add(componentsArrays[i].trim().toLowerCase(VectorApp.getApplicationLocale())); } } } @@ -446,7 +446,7 @@ public String getUniqueDisplayName(List otherDisplayNames) { // for the matrix users, append the matrix id to see the difference if (null == mContact) { - String lowerCaseDisplayname = displayname.toLowerCase(); + String lowerCaseDisplayname = displayname.toLowerCase(VectorApp.getApplicationLocale()); // detect if the username is used by several users int pos = -1; diff --git a/vector/src/main/java/im/vector/adapters/PeopleAdapter.java b/vector/src/main/java/im/vector/adapters/PeopleAdapter.java index 15df1f567a..9559553bde 100644 --- a/vector/src/main/java/im/vector/adapters/PeopleAdapter.java +++ b/vector/src/main/java/im/vector/adapters/PeopleAdapter.java @@ -40,6 +40,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import im.vector.R; +import im.vector.VectorApp; import im.vector.contacts.ContactsManager; import im.vector.util.RoomUtils; import im.vector.util.VectorUtils; @@ -67,7 +68,7 @@ public class PeopleAdapter extends AbsAdapter { * ********************************************************************************************* */ - public PeopleAdapter(final Context context, final OnSelectItemListener listener, final InvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { + public PeopleAdapter(final Context context, final OnSelectItemListener listener, final RoomInvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { super(context, invitationListener, moreActionListener); mListener = listener; @@ -75,15 +76,15 @@ public PeopleAdapter(final Context context, final OnSelectItemListener listener, mNoContactAccessPlaceholder = context.getString(R.string.no_contact_access_placeholder); mNoResultPlaceholder = context.getString(R.string.no_result_placeholder); - mDirectChatsSection = new AdapterSection<>(context.getString(R.string.direct_chats_header), -1, + mDirectChatsSection = new AdapterSection<>(context, context.getString(R.string.direct_chats_header), -1, R.layout.adapter_item_room_view, TYPE_HEADER_DEFAULT, TYPE_ROOM, new ArrayList(), RoomUtils.getRoomsDateComparator(mSession, false)); mDirectChatsSection.setEmptyViewPlaceholder(context.getString(R.string.no_conversation_placeholder), context.getString(R.string.no_result_placeholder)); - mLocalContactsSection = new AdapterSection<>(context.getString(R.string.local_address_book_header), + mLocalContactsSection = new AdapterSection<>(context, context.getString(R.string.local_address_book_header), R.layout.adapter_local_contacts_sticky_header_subview, R.layout.adapter_item_contact_view, TYPE_HEADER_LOCAL_CONTACTS, TYPE_CONTACT, new ArrayList(), ParticipantAdapterItem.alphaComparator); mLocalContactsSection.setEmptyViewPlaceholder(!ContactsManager.getInstance().isContactBookAccessAllowed() ? mNoContactAccessPlaceholder : mNoResultPlaceholder); - mKnownContactsSection = new KnownContactsAdapterSection(context.getString(R.string.user_directory_header), -1, + mKnownContactsSection = new KnownContactsAdapterSection(context, context.getString(R.string.user_directory_header), -1, R.layout.adapter_item_contact_view, TYPE_HEADER_DEFAULT, TYPE_CONTACT, new ArrayList(), null); mKnownContactsSection.setEmptyViewPlaceholder(null, context.getString(R.string.no_result_placeholder)); mKnownContactsSection.setIsHiddenWhenNoFilter(true); @@ -139,7 +140,7 @@ protected void populateViewHolder(int viewType, RecyclerView.ViewHolder viewHold case TYPE_ROOM: final RoomViewHolder roomViewHolder = (RoomViewHolder) viewHolder; final Room room = (Room) getItemForPosition(position); - roomViewHolder.populateViews(mContext, mSession, room, true, false, mMoreActionListener); + roomViewHolder.populateViews(mContext, mSession, room, true, false, mMoreRoomActionListener); roomViewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -247,7 +248,7 @@ public void updateKnownContact(final User user) { private int filterLocalContacts(final String pattern) { if (!TextUtils.isEmpty(pattern)) { List filteredLocalContacts = new ArrayList<>(); - final String formattedPattern = pattern.toLowerCase().trim().toLowerCase(); + final String formattedPattern = pattern.toLowerCase(VectorApp.getApplicationLocale()).trim(); List sectionItems = new ArrayList<>(mLocalContactsSection.getItems()); for (final ParticipantAdapterItem item : sectionItems) { @@ -282,7 +283,7 @@ public void filterAccountKnownContacts(final String pattern) { private int filterKnownContacts(final String pattern) { List filteredKnownContacts = new ArrayList<>(); if (!TextUtils.isEmpty(pattern)) { - final String formattedPattern = pattern.toLowerCase().trim().toLowerCase(); + final String formattedPattern = pattern.trim().toLowerCase(VectorApp.getApplicationLocale()); List sectionItems = new ArrayList<>(mKnownContactsSection.getItems()); for (final ParticipantAdapterItem item : sectionItems) { if (item.startsWith(formattedPattern)) { diff --git a/vector/src/main/java/im/vector/adapters/PublicRoomsAdapterSection.java b/vector/src/main/java/im/vector/adapters/PublicRoomsAdapterSection.java index 6d09b8fa59..af1ede7ade 100644 --- a/vector/src/main/java/im/vector/adapters/PublicRoomsAdapterSection.java +++ b/vector/src/main/java/im/vector/adapters/PublicRoomsAdapterSection.java @@ -16,9 +16,10 @@ package im.vector.adapters; +import android.content.Context; import android.text.TextUtils; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import java.util.Comparator; import java.util.List; @@ -32,9 +33,9 @@ class PublicRoomsAdapterSection extends AdapterSection { // tell if the private boolean mHasMoreResults; - public PublicRoomsAdapterSection(String title, int headerSubViewResId, int contentResId, int headerViewType, + public PublicRoomsAdapterSection(Context context, String title, int headerSubViewResId, int contentResId, int headerViewType, int contentViewType, List items, Comparator comparator) { - super(title, headerSubViewResId, contentResId, headerViewType, contentViewType, items, comparator); + super(context, title, headerSubViewResId, contentResId, headerViewType, contentViewType, items, comparator); } @Override diff --git a/vector/src/main/java/im/vector/adapters/RoomAdapter.java b/vector/src/main/java/im/vector/adapters/RoomAdapter.java index d83506beee..f89a9905ec 100644 --- a/vector/src/main/java/im/vector/adapters/RoomAdapter.java +++ b/vector/src/main/java/im/vector/adapters/RoomAdapter.java @@ -30,7 +30,7 @@ import android.widget.TextView; import org.matrix.androidsdk.data.Room; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import org.matrix.androidsdk.util.Log; import java.util.ArrayList; @@ -61,16 +61,16 @@ public class RoomAdapter extends AbsAdapter { * ********************************************************************************************* */ - public RoomAdapter(final Context context, final OnSelectItemListener listener, final InvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { + public RoomAdapter(final Context context, final OnSelectItemListener listener, final RoomInvitationListener invitationListener, final MoreRoomActionListener moreActionListener) { super(context, invitationListener, moreActionListener); mListener = listener; - mRoomsSection = new AdapterSection<>(context.getString(R.string.rooms_header), -1, + mRoomsSection = new AdapterSection<>(context, context.getString(R.string.rooms_header), -1, R.layout.adapter_item_room_view, TYPE_HEADER_DEFAULT, TYPE_ROOM, new ArrayList(), RoomUtils.getRoomsDateComparator(mSession, false)); mRoomsSection.setEmptyViewPlaceholder(context.getString(R.string.no_room_placeholder), context.getString(R.string.no_result_placeholder)); - mPublicRoomsSection = new PublicRoomsAdapterSection(context.getString(R.string.rooms_directory_header), + mPublicRoomsSection = new PublicRoomsAdapterSection(context, context.getString(R.string.rooms_directory_header), R.layout.adapter_public_room_sticky_header_subview, R.layout.adapter_item_public_room_view, TYPE_HEADER_PUBLIC_ROOM, TYPE_PUBLIC_ROOM, new ArrayList(), null); mPublicRoomsSection.setEmptyViewPlaceholder(context.getString(R.string.no_public_room_placeholder), context.getString(R.string.no_result_placeholder)); @@ -125,7 +125,7 @@ protected void populateViewHolder(int viewType, RecyclerView.ViewHolder viewHold case TYPE_ROOM: final RoomViewHolder roomViewHolder = (RoomViewHolder) viewHolder; final Room room = (Room) getItemForPosition(position); - roomViewHolder.populateViews(mContext, mSession, room, false, false, mMoreActionListener); + roomViewHolder.populateViews(mContext, mSession, room, false, false, mMoreRoomActionListener); roomViewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/vector/src/main/java/im/vector/adapters/InvitationViewHolder.java b/vector/src/main/java/im/vector/adapters/RoomInvitationViewHolder.java similarity index 87% rename from vector/src/main/java/im/vector/adapters/InvitationViewHolder.java rename to vector/src/main/java/im/vector/adapters/RoomInvitationViewHolder.java index 881063a10e..8c33c0b530 100644 --- a/vector/src/main/java/im/vector/adapters/InvitationViewHolder.java +++ b/vector/src/main/java/im/vector/adapters/RoomInvitationViewHolder.java @@ -26,7 +26,7 @@ import butterknife.BindView; import im.vector.R; -public class InvitationViewHolder extends RoomViewHolder { +public class RoomInvitationViewHolder extends RoomViewHolder { @BindView(R.id.recents_invite_reject_button) Button vRejectButton; @@ -34,12 +34,12 @@ public class InvitationViewHolder extends RoomViewHolder { @BindView(R.id.recents_invite_preview_button) Button vPreViewButton; - InvitationViewHolder(View itemView) { + RoomInvitationViewHolder(View itemView) { super(itemView); } void populateViews(final Context context, final MXSession session, final Room room, - final AbsAdapter.InvitationListener invitationListener, final AbsAdapter.MoreRoomActionListener moreRoomActionListener) { + final AbsAdapter.RoomInvitationListener invitationListener, final AbsAdapter.MoreRoomActionListener moreRoomActionListener) { super.populateViews(context, session, room, room.isDirectChatInvitation(), true, moreRoomActionListener); vPreViewButton.setOnClickListener(new View.OnClickListener() { diff --git a/vector/src/main/java/im/vector/adapters/RoomViewHolder.java b/vector/src/main/java/im/vector/adapters/RoomViewHolder.java index 1fcce43981..0ab2432d94 100644 --- a/vector/src/main/java/im/vector/adapters/RoomViewHolder.java +++ b/vector/src/main/java/im/vector/adapters/RoomViewHolder.java @@ -40,7 +40,7 @@ import im.vector.util.VectorUtils; public class RoomViewHolder extends RecyclerView.ViewHolder { - private static final String LOG_TAG = PeopleAdapter.class.getSimpleName(); + private static final String LOG_TAG = RoomViewHolder.class.getSimpleName(); @BindView(R.id.room_avatar) ImageView vRoomAvatar; diff --git a/vector/src/main/java/im/vector/adapters/VectorGroupsListAdapter.java b/vector/src/main/java/im/vector/adapters/VectorGroupsListAdapter.java new file mode 100644 index 0000000000..7d3afeed4f --- /dev/null +++ b/vector/src/main/java/im/vector/adapters/VectorGroupsListAdapter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.adapters; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.rest.model.group.GroupProfile; + +import java.util.HashMap; +import java.util.Map; + +import im.vector.R; +import im.vector.activity.VectorGroupDetailsActivity; +import im.vector.util.VectorUtils; + +/** + * An adapter which can display groups list + */ +public class VectorGroupsListAdapter extends ArrayAdapter { + + private final Context mContext; + private final LayoutInflater mLayoutInflater; + private final int mLayoutResourceId; + private final MXSession mSession; + + private Map mGroupByGroupId = new HashMap<>(); + + public VectorGroupsListAdapter(Context context, int layoutResourceId, MXSession session) { + super(context, layoutResourceId); + mContext = context; + mLayoutResourceId = layoutResourceId; + mLayoutInflater = LayoutInflater.from(mContext); + mSession = session; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mLayoutInflater.inflate(mLayoutResourceId, parent, false); + } + + final String groupId = getItem(position); + Group group = mGroupByGroupId.get(groupId); + + if (null == group) { + group = mSession.getGroupsManager().getGroup(groupId); + + if (null != group) { + mGroupByGroupId.put(groupId, group); + } + } + + boolean needRefresh = (null == group); + + if (null == group) { + group = new Group(groupId); + } + + convertView.findViewById(R.id.group_members_count).setVisibility(View.GONE); + + final TextView groupName = convertView.findViewById(R.id.group_name); + groupName.setTag(groupId); + groupName.setText(group.getDisplayName()); + groupName.setTypeface(null, Typeface.NORMAL); + + final ImageView groupAvatar = convertView.findViewById(R.id.room_avatar); + VectorUtils.loadGroupAvatar(mContext, mSession, groupAvatar, group); + + final TextView groupTopic = convertView.findViewById(R.id.group_topic); + groupTopic.setText(group.getShortDescription()); + + convertView.findViewById(R.id.group_more_action_click_area).setVisibility(View.INVISIBLE); + convertView.findViewById(R.id.group_more_action_anchor).setVisibility(View.INVISIBLE); + convertView.findViewById(R.id.group_more_action_ic).setVisibility(View.INVISIBLE); + + if (needRefresh) { + mSession.getGroupsManager().getGroupProfile(groupId, new ApiCallback() { + @Override + public void onSuccess(GroupProfile groupProfile) { + if (TextUtils.equals((String) groupName.getTag(), groupId)) { + Group updatedGroup = mGroupByGroupId.get(groupId); + + if (null == updatedGroup) { + updatedGroup = new Group(groupId); + updatedGroup.setGroupProfile(groupProfile); + mGroupByGroupId.put(groupId, updatedGroup); + } + + groupName.setText(updatedGroup.getDisplayName()); + VectorUtils.loadGroupAvatar(mContext, mSession, groupAvatar, updatedGroup); + groupTopic.setText(updatedGroup.getShortDescription()); + } + } + + @Override + public void onNetworkError(Exception e) { + } + + @Override + public void onMatrixError(MatrixError e) { + } + + @Override + public void onUnexpectedError(Exception e) { + } + }); + } + + convertView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(mContext, VectorGroupDetailsActivity.class); + intent.putExtra(VectorGroupDetailsActivity.EXTRA_GROUP_ID, groupId); + intent.putExtra(VectorGroupDetailsActivity.EXTRA_MATRIX_ID, mSession.getCredentials().userId); + mContext.startActivity(intent); + } + }); + + + convertView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("", groupId); + clipboard.setPrimaryClip(clip); + + Toast.makeText(mContext, mContext.getResources().getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); + return true; + } + }); + + return convertView; + } +} diff --git a/vector/src/main/java/im/vector/adapters/VectorMediasViewerAdapter.java b/vector/src/main/java/im/vector/adapters/VectorMediasViewerAdapter.java index 7e3cc1d8ac..abd648ddbf 100755 --- a/vector/src/main/java/im/vector/adapters/VectorMediasViewerAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorMediasViewerAdapter.java @@ -30,6 +30,7 @@ import android.text.TextUtils; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.model.crypto.EncryptedFileInfo; import org.matrix.androidsdk.util.Log; import android.view.LayoutInflater; @@ -47,7 +48,7 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.listeners.MXMediaDownloadListener; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.util.ImageUtils; import org.matrix.androidsdk.util.JsonUtils; import org.matrix.androidsdk.view.PieFractionView; @@ -112,6 +113,8 @@ public void setPrimaryItem(ViewGroup container, final int position, Object objec final View view = (View) object; mLatestPrimaryView = view; + view.findViewById(R.id.media_download_failed).setVisibility(View.GONE); + view.post(new Runnable() { @Override public void run() { @@ -125,13 +128,22 @@ public void run() { if (mHighResMediaIndex.indexOf(position) < 0) { downloadHighResMedia(view, position); } else if (position == mAutoPlayItemAt) { - SlidableMediaInfo mediaInfo = mMediasMessagesList.get(position); + final SlidableMediaInfo mediaInfo = mMediasMessagesList.get(position); if (mediaInfo.mMessageType.equals(Message.MSGTYPE_VIDEO)) { final VideoView videoView = view.findViewById(R.id.media_slider_videoview); - playVideo(view, videoView, mediaInfo.mMediaUrl, mediaInfo.mMimeType); - } + if (mMediasCache.isMediaCached(mediaInfo.mMediaUrl, mediaInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File file) { + if (null != file) { + playVideo(view, videoView, file, mediaInfo.mMimeType); + } + } + }); + } + } mAutoPlayItemAt = -1; } } @@ -189,21 +201,28 @@ private void downloadVideo(final View view, final int position, boolean force) { final VideoView videoView = view.findViewById(R.id.media_slider_videoview); final ImageView thumbView = view.findViewById(R.id.media_slider_video_thumbnail); final PieFractionView pieFractionView = view.findViewById(R.id.media_slider_piechart); + final View downloadFailedView = view.findViewById(R.id.media_download_failed); final SlidableMediaInfo mediaInfo = mMediasMessagesList.get(position); final String loadingUri = mediaInfo.mMediaUrl; final String thumbnailUrl = mediaInfo.mThumbnailUrl; // check if the media has been downloaded - File file = mMediasCache.mediaCacheFile(loadingUri, mediaInfo.mMimeType); - if (null != file) { - mHighResMediaIndex.add(position); - loadVideo(position, view, thumbnailUrl, Uri.fromFile(file).toString(), mediaInfo.mMimeType); + if (mMediasCache.isMediaCached(loadingUri, mediaInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(loadingUri, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File file) { + if (null != file) { + mHighResMediaIndex.add(position); + loadVideo(position, view, thumbnailUrl, Uri.fromFile(file).toString(), mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo); - if (position == mAutoPlayItemAt) { - playVideo(view, videoView, mediaInfo.mMediaUrl, mediaInfo.mMimeType); - } - mAutoPlayItemAt = -1; + if (position == mAutoPlayItemAt) { + playVideo(view, videoView, file, mediaInfo.mMimeType); + } + mAutoPlayItemAt = -1; + } + } + }); return; } @@ -230,6 +249,8 @@ public void onDownloadError(String downloadId, JsonElement jsonElement) { if ((null != error) && error.isSupportedErrorCode()) { Toast.makeText(VectorMediasViewerAdapter.this.mContext, error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } + + downloadFailedView.setVisibility(View.VISIBLE); } @Override @@ -244,25 +265,34 @@ public void onDownloadComplete(String aDownloadId) { if (aDownloadId.equals(pieFractionView.getTag())) { pieFractionView.setVisibility(View.GONE); - final File mediaFile = mMediasCache.mediaCacheFile(loadingUri, mediaInfo.mMimeType); - - if (null != mediaFile) { - mHighResMediaIndex.add(position); - - Uri uri = Uri.fromFile(mediaFile); - final String newHighResUri = uri.toString(); - thumbView.post(new Runnable() { + // check if the media has been downloaded + if (mMediasCache.isMediaCached(loadingUri, mediaInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(loadingUri, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { @Override - public void run() { - loadVideo(position, view, thumbnailUrl, newHighResUri, mediaInfo.mMimeType); - - if (position == mAutoPlayItemAt) { - playVideo(view, videoView, mediaInfo.mMediaUrl, mediaInfo.mMimeType); - mAutoPlayItemAt = -1; + public void onSuccess(final File mediaFile) { + if (null != mediaFile) { + mHighResMediaIndex.add(position); + + Uri uri = Uri.fromFile(mediaFile); + final String newHighResUri = uri.toString(); + + thumbView.post(new Runnable() { + @Override + public void run() { + loadVideo(position, view, thumbnailUrl, newHighResUri, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo); + + if (position == mAutoPlayItemAt) { + playVideo(view, videoView, mediaFile, mediaInfo.mMimeType); + mAutoPlayItemAt = -1; + } + } + }); } } }); + } else { + downloadFailedView.setVisibility(View.VISIBLE); } } } @@ -279,6 +309,8 @@ public void run() { private void downloadHighResPict(final View view, final int position) { final WebView webView = view.findViewById(R.id.media_slider_image_webview); final PieFractionView pieFractionView = view.findViewById(R.id.media_slider_piechart); + final View downloadFailedView = view.findViewById(R.id.media_download_failed); + final SlidableMediaInfo imageInfo = mMediasMessagesList.get(position); final String viewportContent = "width=640"; final String loadingUri = imageInfo.mMediaUrl; @@ -291,11 +323,17 @@ private void downloadHighResPict(final View view, final int position) { pieFractionView.setFraction(mMediasCache.getProgressValueForDownloadId(downloadId)); mMediasCache.addDownloadListener(downloadId, new MXMediaDownloadListener() { @Override - public void onDownloadError(String downloadId, JsonElement jsonElement) { - MatrixError error = JsonUtils.toMatrixError(jsonElement); + public void onDownloadError(String aDownloadId, JsonElement jsonElement) { + if (aDownloadId.equals(downloadId)) { + pieFractionView.setVisibility(View.GONE); - if ((null != error) && error.isSupportedErrorCode()) { - Toast.makeText(VectorMediasViewerAdapter.this.mContext, error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + MatrixError error = JsonUtils.toMatrixError(jsonElement); + + if (null != error) { + Toast.makeText(VectorMediasViewerAdapter.this.mContext, error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + + downloadFailedView.setVisibility(View.VISIBLE); } } @@ -310,22 +348,30 @@ public void onDownloadProgress(String aDownloadId, DownloadStats stats) { public void onDownloadComplete(String aDownloadId) { if (aDownloadId.equals(downloadId)) { pieFractionView.setVisibility(View.GONE); - final File mediaFile = mMediasCache.mediaCacheFile(loadingUri, imageInfo.mMimeType); - if (null != mediaFile) { - mHighResMediaIndex.add(position); - - Uri uri = Uri.fromFile(mediaFile); - final String newHighResUri = uri.toString(); - - webView.post(new Runnable() { + if (mMediasCache.isMediaCached(loadingUri, imageInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(loadingUri, imageInfo.mMimeType, imageInfo.mEncryptedFileInfo, new SimpleApiCallback() { @Override - public void run() { - Uri mediaUri = Uri.parse(newHighResUri); - // refresh the UI - loadImage(webView, mediaUri, viewportContent, computeCss(newHighResUri, VectorMediasViewerAdapter.this.mMaxImageWidth, VectorMediasViewerAdapter.this.mMaxImageHeight, imageInfo.mRotationAngle)); + public void onSuccess(File mediaFile) { + if (null != mediaFile) { + mHighResMediaIndex.add(position); + + Uri uri = Uri.fromFile(mediaFile); + final String newHighResUri = uri.toString(); + + webView.post(new Runnable() { + @Override + public void run() { + Uri mediaUri = Uri.parse(newHighResUri); + // refresh the UI + loadImage(webView, mediaUri, viewportContent, computeCss(newHighResUri, VectorMediasViewerAdapter.this.mMaxImageWidth, VectorMediasViewerAdapter.this.mMaxImageHeight, imageInfo.mRotationAngle)); + } + }); + } } }); + } else { + downloadFailedView.setVisibility(View.VISIBLE); } } } @@ -339,13 +385,15 @@ public boolean isViewFromObject(View view, Object object) { } @Override - public Object instantiateItem(ViewGroup container, final int position) { - View view = mLayoutInflater.inflate(R.layout.adapter_vector_medias_viewer, null, false); + public Object instantiateItem(final ViewGroup container, final int position) { + final View view = mLayoutInflater.inflate(R.layout.adapter_vector_medias_viewer, null, false); // hide the pie chart final PieFractionView pieFractionView = view.findViewById(R.id.media_slider_piechart); pieFractionView.setVisibility(View.GONE); + view.findViewById(R.id.media_download_failed).setVisibility(View.GONE); + final WebView imageWebView = view.findViewById(R.id.media_slider_image_webview); final View videoLayout = view.findViewById(R.id.media_slider_videolayout); final ImageView thumbView = view.findViewById(R.id.media_slider_video_thumbnail); @@ -393,33 +441,42 @@ public boolean onLongClick(View v) { } final String mimeType = mediaInfo.mMimeType; - File mediaFile = mMediasCache.mediaCacheFile(mediaUrl, mimeType); + int width = -1; + int height = -1; // is the high picture already downloaded ? - if (null != mediaFile) { + if (mMediasCache.isMediaCached(mediaUrl, mimeType)) { if (mHighResMediaIndex.indexOf(position) < 0) { mHighResMediaIndex.add(position); } } else { - // try to retrieve the thumbnail - mediaFile = mMediasCache.mediaCacheFile(mediaUrl, mMaxImageWidth, mMaxImageHeight, null); + width = mMaxImageWidth; + height = mMaxImageHeight; } // the thumbnail is not yet downloaded - if (null == mediaFile) { + if (!mMediasCache.isMediaCached(mediaUrl, width, height, mimeType)) { // display nothing container.addView(view, 0); return view; } - String mediaUri = "file://" + mediaFile.getPath(); + mMediasCache.createTmpMediaFile(mediaUrl, width, height, mimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File mediaFile) { + if (null != mediaFile) { + String mediaUri = "file://" + mediaFile.getPath(); + + String css = computeCss(mediaUri, mMaxImageWidth, mMaxImageHeight, rotationAngle); + final String viewportContent = "width=640"; + loadImage(imageWebView, Uri.parse(mediaUri), viewportContent, css); + container.addView(view, 0); + } + } + }); - String css = computeCss(mediaUri, mMaxImageWidth, mMaxImageHeight, rotationAngle); - final String viewportContent = "width=640"; - loadImage(imageWebView, Uri.parse(mediaUri), viewportContent, css); - container.addView(view, 0); } else { - loadVideo(position, view, mediaInfo.mThumbnailUrl, mediaUrl, mediaInfo.mMimeType); + loadVideo(position, view, mediaInfo.mThumbnailUrl, mediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo); container.addView(view, 0); } @@ -494,15 +551,11 @@ public void stopPlayingVideo() { * * @param pageView the pageView * @param videoView the video view - * @param videoUrl the video Url + * @param videoFile the video file * @param videoMimeType the video mime type */ - private void playVideo(View pageView, VideoView videoView, String videoUrl, String videoMimeType) { - // init the video view only if there is a valid file - // check if the media has been downloaded - File srcFile = mMediasCache.mediaCacheFile(videoUrl, videoMimeType); - - if ((null != srcFile) && srcFile.exists()) { + private void playVideo(View pageView, VideoView videoView, File videoFile, String videoMimeType) { + if ((null != videoFile) && videoFile.exists()) { try { stopPlayingVideo(); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(videoMimeType); @@ -525,7 +578,7 @@ private void playVideo(View pageView, VideoView videoView, String videoUrl, Stri if (!dstFile.exists()) { dstFile.createNewFile(); - inputStream = new FileInputStream(srcFile); + inputStream = new FileInputStream(videoFile); outputStream = new FileOutputStream(dstFile); byte[] buffer = new byte[1024 * 10]; @@ -567,16 +620,21 @@ private void playVideo(View pageView, VideoView videoView, String videoUrl, Stri */ private void downloadMedia() { final SlidableMediaInfo mediaInfo = mMediasMessagesList.get(mLatestPrimaryItemPosition); - File file = mMediasCache.mediaCacheFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType); - if (null != file) { - CommonActivityUtils.saveMediaIntoDownloads(mContext, file, null, mediaInfo.mMimeType, new SimpleApiCallback() { + if (mMediasCache.isMediaCached(mediaInfo.mMediaUrl, mediaInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { @Override - public void onSuccess(String path) { - Toast.makeText(mContext, mContext.getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + public void onSuccess(File file) { + if (null != file) { + CommonActivityUtils.saveMediaIntoDownloads(mContext, file, null, mediaInfo.mMimeType, new SimpleApiCallback() { + @Override + public void onSuccess(String path) { + Toast.makeText(mContext, mContext.getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + } + }); + } } }); - } else { downloadVideo(mLatestPrimaryView, mLatestPrimaryItemPosition, true); final String downloadId = mMediasCache.downloadMedia(mContext, mSession.getHomeServerConfig(), mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo); @@ -595,13 +653,18 @@ public void onDownloadError(String downloadId, JsonElement jsonElement) { @Override public void onDownloadComplete(String aDownloadId) { if (aDownloadId.equals(downloadId)) { - File file = mMediasCache.mediaCacheFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType); - if (null != file) { - - CommonActivityUtils.saveMediaIntoDownloads(mContext, file, null, mediaInfo.mMimeType, new SimpleApiCallback() { + if (mMediasCache.isMediaCached(mediaInfo.mMediaUrl, mediaInfo.mMimeType)) { + mMediasCache.createTmpMediaFile(mediaInfo.mMediaUrl, mediaInfo.mMimeType, mediaInfo.mEncryptedFileInfo, new SimpleApiCallback() { @Override - public void onSuccess(String path) { - Toast.makeText(mContext, mContext.getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + public void onSuccess(File file) { + if (null != file) { + CommonActivityUtils.saveMediaIntoDownloads(mContext, file, null, mediaInfo.mMimeType, new SimpleApiCallback() { + @Override + public void onSuccess(String path) { + Toast.makeText(mContext, mContext.getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + } + }); + } } }); } @@ -644,7 +707,7 @@ public void onClick(DialogInterface dialog, int which) { * @param videoUrl the video Url * @param videoMimeType the video mime type */ - private void loadVideo(final int position, final View view, final String thumbnailUrl, final String videoUrl, final String videoMimeType) { + private void loadVideo(final int position, final View view, final String thumbnailUrl, final String videoUrl, final String videoMimeType, final EncryptedFileInfo encryptedFileInfo) { final VideoView videoView = view.findViewById(R.id.media_slider_videoview); final ImageView thumbView = view.findViewById(R.id.media_slider_video_thumbnail); final ImageView playView = view.findViewById(R.id.media_slider_video_playView); @@ -684,12 +747,17 @@ public boolean onError(MediaPlayer mp, int what, int extra) { playView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - // init the video view only if there is a valid file - // check if the media has been downloaded - File srcFile = mMediasCache.mediaCacheFile(videoUrl, videoMimeType); + if (mMediasCache.isMediaCached(videoUrl, videoMimeType)) { + + mMediasCache.createTmpMediaFile(videoUrl, videoMimeType, encryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File file) { + if (null != file) { + playVideo(view, videoView, file, videoMimeType); + } + } + }); - if (null != srcFile) { - playVideo(view, videoView, videoUrl, videoMimeType); } else { mAutoPlayItemAt = position; downloadVideo(view, position); diff --git a/vector/src/main/java/im/vector/adapters/VectorMemberDetailsAdapter.java b/vector/src/main/java/im/vector/adapters/VectorMemberDetailsAdapter.java index 9d90f84a74..a6ad8b55c6 100644 --- a/vector/src/main/java/im/vector/adapters/VectorMemberDetailsAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorMemberDetailsAdapter.java @@ -18,6 +18,7 @@ package im.vector.adapters; import android.content.Context; +import android.content.DialogInterface; import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; @@ -409,7 +410,34 @@ public void onClick(View view) { @Override public void onClick(View view) { if (null != mActionListener) { - mActionListener.performItemAction(currentItem.mActionType); + if (VectorMemberDetailsActivity.ITEM_ACTION_KICK == currentItem.mActionType + || VectorMemberDetailsActivity.ITEM_ACTION_BAN == currentItem.mActionType) { + android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(view.getContext()); + builder.setTitle(R.string.dialog_title_confirmation); + + if (VectorMemberDetailsActivity.ITEM_ACTION_KICK == currentItem.mActionType) { + builder.setMessage(view.getContext().getString(R.string.room_participants_kick_prompt_msg)); + } else { + builder.setMessage(view.getContext().getString(R.string.room_participants_ban_prompt_msg)); + } + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mActionListener.performItemAction(currentItem.mActionType); + } + }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // nothing to do + } + }); + + builder.show(); + } else { + mActionListener.performItemAction(currentItem.mActionType); + } } } }); diff --git a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapter.java b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapter.java index fefd9b8f39..e47bd99010 100755 --- a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapter.java @@ -44,6 +44,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.TextView; @@ -54,12 +55,13 @@ import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.data.RoomState; import org.matrix.androidsdk.db.MXMediasCache; -import org.matrix.androidsdk.rest.model.EncryptedEventContent; +import org.matrix.androidsdk.rest.model.URLPreview; +import org.matrix.androidsdk.rest.model.crypto.EncryptedEventContent; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.EventContent; -import org.matrix.androidsdk.rest.model.FileMessage; -import org.matrix.androidsdk.rest.model.ImageMessage; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.FileMessage; +import org.matrix.androidsdk.rest.model.message.ImageMessage; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.rest.model.PowerLevels; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.util.EventDisplay; @@ -78,6 +80,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -92,6 +95,8 @@ import im.vector.util.PreferencesManager; import im.vector.util.RiotEventDisplay; import im.vector.util.ThemeUtils; +import im.vector.util.VectorImageGetter; +import im.vector.util.VectorMarkdownParser; import im.vector.widgets.WidgetsManager; /** @@ -142,7 +147,8 @@ public class VectorMessagesAdapter extends AbstractMessagesAdapter { static final int ROW_TYPE_HIDDEN = 7; static final int ROW_TYPE_ROOM_MEMBER = 8; static final int ROW_TYPE_EMOJI = 9; - static final int NUM_ROW_TYPES = 10; + static final int ROW_TYPE_CODE = 10; + static final int NUM_ROW_TYPES = 11; final Context mContext; private final HashMap mRowTypeToLayoutId = new HashMap<>(); @@ -212,6 +218,7 @@ public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache m R.layout.adapter_item_vector_message_image_video, R.layout.adapter_item_vector_message_merge, R.layout.adapter_item_vector_message_emoji, + R.layout.adapter_item_vector_message_code, mediasCache); } @@ -240,6 +247,7 @@ public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache m int videoResLayoutId, int mergeResLayoutId, int emojiResLayoutId, + int codeResLayoutId, MXMediasCache mediasCache) { super(context, 0); mContext = context; @@ -253,6 +261,7 @@ public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache m mRowTypeToLayoutId.put(ROW_TYPE_MERGE, mergeResLayoutId); mRowTypeToLayoutId.put(ROW_TYPE_HIDDEN, R.layout.adapter_item_vector_hidden_message); mRowTypeToLayoutId.put(ROW_TYPE_EMOJI, emojiResLayoutId); + mRowTypeToLayoutId.put(ROW_TYPE_CODE, codeResLayoutId); mMediasCache = mediasCache; mLayoutInflater = LayoutInflater.from(mContext); @@ -286,7 +295,7 @@ public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache m // helpers mMediasHelper = new VectorMessagesAdapterMediasHelper(context, mSession, mMaxImageWidth, mMaxImageHeight, mNotSentMessageTextColor, mDefaultMessageTextColor); - mHelper = new VectorMessagesAdapterHelper(context, mSession); + mHelper = new VectorMessagesAdapterHelper(context, mSession, this); mLocale = VectorApp.getApplicationLocale(); @@ -311,13 +320,7 @@ public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache m @SuppressWarnings("deprecation") private void getScreenSize(Point size) { WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { - display.getSize(size); - } else { - size.set(display.getWidth(), display.getHeight()); - } + wm.getDefaultDisplay().getSize(size); } /** @@ -425,11 +428,11 @@ public void add(MessageRow row) { @Override public void add(MessageRow row, boolean refresh) { - // ensure that notifyDataSetChanged is not called - // it seems that setNotifyOnChange is reinitialized to true; - setNotifyOnChange(false); - if (isSupportedRow(row)) { + // ensure that notifyDataSetChanged is not called + // it seems that setNotifyOnChange is reinitialized to true; + setNotifyOnChange(false); + if (mIsSearchMode) { mLiveMessagesRowList.add(row); } else { @@ -446,8 +449,6 @@ public void add(MessageRow row, boolean refresh) { } else { setNotifyOnChange(true); } - } else { - setNotifyOnChange(true); } } @@ -667,8 +668,19 @@ public View getView(int position, View convertView, ViewGroup parent) { final View inflatedView; int viewType = getItemViewType(position); + // when the user scrolls quickly + // it seems that the recycled view does not have the right layout. + // check it + if (null != convertView) { + if (viewType != (int)convertView.getTag()) { + Log.e(LOG_TAG, "## getView() : invalid view type : got " + convertView.getTag() + " instead of " + viewType); + convertView = null; + } + } + switch (viewType) { case ROW_TYPE_EMOJI: + case ROW_TYPE_CODE: case ROW_TYPE_TEXT: inflatedView = getTextView(viewType, position, convertView, parent); break; @@ -702,6 +714,7 @@ public View getView(int position, View convertView, ViewGroup parent) { if (null != inflatedView) { inflatedView.setBackgroundColor(Color.TRANSPARENT); + inflatedView.setTag(viewType); } displayE2eIcon(inflatedView, position); @@ -942,6 +955,8 @@ private int getItemViewType(Event event) { if (Message.MSGTYPE_TEXT.equals(msgType)) { if (containsOnlyEmojis(message.body)) { viewType = ROW_TYPE_EMOJI; + } else if (!TextUtils.isEmpty(message.formatted_body) && mHelper.containsFencedCodeBlocks(message)) { + viewType = ROW_TYPE_CODE; } else { viewType = ROW_TYPE_TEXT; } @@ -1090,7 +1105,7 @@ public boolean onLongClick(View v) { } // selection mode - manageSelectionMode(convertView, event); + manageSelectionMode(convertView, event, msgType); // read marker setReadMarker(convertView, row, isMergedView, avatarLayoutView, bodyLayoutView); @@ -1125,17 +1140,29 @@ private View getTextView(final int viewType, final int position, View convertVie CharSequence textualDisplay = display.getTextualDisplay(); SpannableString body = new SpannableString((null == textualDisplay) ? "" : textualDisplay); - final TextView bodyTextView = convertView.findViewById(R.id.messagesAdapter_body); - - // cannot refresh it - if (null == bodyTextView) { - Log.e(LOG_TAG, "getTextView : invalid layout"); - return convertView; - } boolean shouldHighlighted = (null != mVectorMessagesAdapterEventsListener) && mVectorMessagesAdapterEventsListener.shouldHighlightEvent(event); - highlightPattern(bodyTextView, body, TextUtils.equals(Message.FORMAT_MATRIX_HTML, message.format) ? mHelper.getSanitisedHtml(message.formatted_body) : null, mPattern, shouldHighlighted); + final List textViews; + + if (ROW_TYPE_CODE == viewType) { + textViews = populateRowTypeCode(message, convertView, shouldHighlighted); + } else { + final TextView bodyTextView = convertView.findViewById(R.id.messagesAdapter_body); + + // cannot refresh it + if (null == bodyTextView) { + Log.e(LOG_TAG, "getTextView : invalid layout"); + return convertView; + } + + highlightPattern(bodyTextView, body, + TextUtils.equals(Message.FORMAT_MATRIX_HTML, message.format) ? mHelper.getSanitisedHtml(message.formatted_body) : null, + mPattern, shouldHighlighted); + + textViews = new ArrayList<>(); + textViews.add(bodyTextView); + } int textColor; @@ -1149,12 +1176,18 @@ private View getTextView(final int viewType, final int position, View convertVie textColor = shouldHighlighted ? mHighlightMessageTextColor : mDefaultMessageTextColor; } - bodyTextView.setTextColor(textColor); + for (final TextView tv : textViews) { + tv.setTextColor(textColor); + } View textLayout = convertView.findViewById(R.id.messagesAdapter_text_layout); - this.manageSubView(position, convertView, textLayout, ROW_TYPE_TEXT); + this.manageSubView(position, convertView, textLayout, viewType); + + for (final TextView tv : textViews) { + addContentViewListeners(convertView, tv, position, viewType); + } - addContentViewListeners(convertView, bodyTextView, position); + mHelper.manageURLPreviews(message, convertView, event.eventId); } catch (Exception e) { Log.e(LOG_TAG, "## getTextView() failed : " + e.getMessage()); } @@ -1162,6 +1195,56 @@ private View getTextView(final int viewType, final int position, View convertVie return convertView; } + /** + * For ROW_TYPE_CODE message which may contain mixture of + * fenced and inline code blocks and non-code (issue 145) + */ + private List populateRowTypeCode(final Message message, + final View convertView, + final boolean shouldHighlighted) { + final List textViews = new ArrayList<>(); + final LinearLayout container = convertView.findViewById(R.id.messages_container); + + // remove older blocks + container.removeAllViews(); + + final String[] blocks = mHelper.getFencedCodeBlocks(message); + final String START_FB = VectorMessagesAdapterHelper.START_FENCED_BLOCK; + final String END_FB = VectorMessagesAdapterHelper.END_FENCED_BLOCK; + for (final String block : blocks) { + if (block.startsWith(START_FB) && block.endsWith(END_FB)) { + // Fenced block + final String minusTags = block + .substring(START_FB.length(), block.length() - END_FB.length()) + .replace("\n", "
") + .replace(" ", " "); + final View blockView = mLayoutInflater.inflate(R.layout.adapter_item_vector_message_code_block, null); + container.addView(blockView); + final TextView tv = blockView.findViewById(R.id.messagesAdapter_body); + highlightPattern(tv, new SpannableString(minusTags), + TextUtils.equals(Message.FORMAT_MATRIX_HTML, message.format) ? mHelper.getSanitisedHtml(minusTags) : null, + mPattern, shouldHighlighted); + + mHelper.highlightFencedCode(tv); + textViews.add(tv); + + ((View) tv.getParent()).setBackgroundColor(ThemeUtils.getColor(mContext, R.attr.markdown_block_background_color)); + } else { + // Not a fenced block + final TextView tv = (TextView) mLayoutInflater.inflate(R.layout.adapter_item_vector_message_code_text, null); + + highlightPattern(tv, new SpannableString(block), + TextUtils.equals(Message.FORMAT_MATRIX_HTML, message.format) ? mHelper.getSanitisedHtml(block) : null, + mPattern, shouldHighlighted); + + container.addView(tv); + textViews.add(tv); + } + } + + return textViews; + } + /** * Image / Video message management * @@ -1226,7 +1309,7 @@ private View getImageVideoView(int type, final int position, View convertView, V this.manageSubView(position, convertView, imageLayout, type); ImageView imageView = convertView.findViewById(R.id.messagesAdapter_image); - addContentViewListeners(convertView, imageView, position); + addContentViewListeners(convertView, imageView, position, type); } catch (Exception e) { Log.e(LOG_TAG, "## getImageVideoView() failed : " + e.getMessage()); } @@ -1275,7 +1358,7 @@ private View getNoticeRoomMemberView(final int viewType, final int position, Vie View textLayout = convertView.findViewById(R.id.messagesAdapter_text_layout); this.manageSubView(position, convertView, textLayout, viewType); - addContentViewListeners(convertView, noticeTextView, position); + addContentViewListeners(convertView, noticeTextView, position, viewType); // android seems having a big issue when the text is too long and an alpha !=1 is applied: // ---> the text is not displayed. @@ -1286,6 +1369,9 @@ private View getNoticeRoomMemberView(final int viewType, final int position, Vie // the patch apply the alpha to the text color but it does not work for the hyperlinks. noticeTextView.setAlpha(1.0f); noticeTextView.setTextColor(getNoticeTextColor()); + + Message message = JsonUtils.toMessage(msg.getContent()); + mHelper.manageURLPreviews(message, convertView, msg.eventId); } catch (Exception e) { Log.e(LOG_TAG, "## getNoticeRoomMemberView() failed : " + e.getMessage()); } @@ -1352,7 +1438,9 @@ private View getEmoteView(final int position, View convertView, ViewGroup parent View textLayout = convertView.findViewById(R.id.messagesAdapter_text_layout); this.manageSubView(position, convertView, textLayout, ROW_TYPE_EMOTE); - addContentViewListeners(convertView, emoteTextView, position); + addContentViewListeners(convertView, emoteTextView, position, ROW_TYPE_EMOTE); + + mHelper.manageURLPreviews(message, convertView, event.eventId); } catch (Exception e) { Log.e(LOG_TAG, "## getEmoteView() failed : " + e.getMessage()); } @@ -1403,7 +1491,7 @@ private View getFileView(final int position, View convertView, ViewGroup parent) View fileLayout = convertView.findViewById(R.id.messagesAdapter_file_layout); this.manageSubView(position, convertView, fileLayout, ROW_TYPE_FILE); - addContentViewListeners(convertView, fileTextView, position); + addContentViewListeners(convertView, fileTextView, position, ROW_TYPE_FILE); } catch (Exception e) { Log.e(LOG_TAG, "## getFileView() failed " + e.getMessage()); } @@ -1559,51 +1647,56 @@ private void highlightPattern(TextView textView, Spannable text, String htmlForm * @return true if should be added */ private boolean isSupportedRow(MessageRow row) { - boolean isSupported = VectorMessagesAdapterHelper.isDisplayableEvent(mContext, row); - - if (isSupported) { - String eventId = row.getEvent().eventId; - - MessageRow currentRow = mEventRowMap.get(eventId); + Event event = row.getEvent(); - // the row should be added only if the message has not been received - isSupported = (null == currentRow); + // sanity checks + if ((null == event) || (null == event.eventId)) { + Log.e(LOG_TAG, "## isSupportedRow() : invalid row"); + return false; + } - // check if the message is already received - if (null != currentRow) { - // waiting for echo - // the message is displayed as sent event if the echo has not been received - // it avoids displaying a pending message whereas the message has been sent - if (currentRow.getEvent().getAge() == Event.DUMMY_EVENT_AGE) { - currentRow.updateEvent(row.getEvent()); - } + String eventId = event.eventId; + MessageRow currentRow = mEventRowMap.get(eventId); + + if (null != currentRow) { + // waiting for echo + // the message is displayed as sent event if the echo has not been received + // it avoids displaying a pending message whereas the message has been sent + if (event.getAge() == Event.DUMMY_EVENT_AGE) { + currentRow.updateEvent(event); + Log.d(LOG_TAG, "## isSupportedRow() : update the timestamp of " + eventId); + } else { + Log.e(LOG_TAG, "## isSupportedRow() : the event " + eventId + " has already been received"); } + return false; + } - if (TextUtils.equals(row.getEvent().getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { - RoomMember roomMember = JsonUtils.toRoomMember(row.getEvent().getContent()); - String membership = roomMember.membership; + boolean isSupported = VectorMessagesAdapterHelper.isDisplayableEvent(mContext, row); - if (PreferencesManager.hideJoinLeaveMessages(mContext)) { - isSupported = !TextUtils.equals(membership, RoomMember.MEMBERSHIP_LEAVE) && !TextUtils.equals(membership, RoomMember.MEMBERSHIP_JOIN); - } + if (isSupported && TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { + RoomMember roomMember = JsonUtils.toRoomMember(event.getContent()); + String membership = roomMember.membership; - if (isSupported && PreferencesManager.hideAvatarDisplayNameChangeMessages(mContext) && TextUtils.equals(membership, RoomMember.MEMBERSHIP_JOIN)) { - EventContent eventContent = JsonUtils.toEventContent(row.getEvent().getContentAsJsonObject()); - EventContent prevEventContent = row.getEvent().getPrevContent(); + if (PreferencesManager.hideJoinLeaveMessages(mContext)) { + isSupported = !TextUtils.equals(membership, RoomMember.MEMBERSHIP_LEAVE) && !TextUtils.equals(membership, RoomMember.MEMBERSHIP_JOIN); + } - String senderDisplayName = eventContent.displayname; - String prevUserDisplayName = null; - String avatar = eventContent.avatar_url; - String prevAvatar = null; + if (isSupported && PreferencesManager.hideAvatarDisplayNameChangeMessages(mContext) && TextUtils.equals(membership, RoomMember.MEMBERSHIP_JOIN)) { + EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject()); + EventContent prevEventContent = event.getPrevContent(); - if ((null != prevEventContent)) { - prevUserDisplayName = prevEventContent.displayname; - prevAvatar = prevEventContent.avatar_url; - } + String senderDisplayName = eventContent.displayname; + String prevUserDisplayName = null; + String avatar = eventContent.avatar_url; + String prevAvatar = null; - // !Updated display name && same avatar - isSupported = TextUtils.equals(prevUserDisplayName, senderDisplayName) && TextUtils.equals(avatar, prevAvatar); + if ((null != prevEventContent)) { + prevUserDisplayName = prevEventContent.displayname; + prevAvatar = prevEventContent.avatar_url; } + + // !Updated display name && same avatar + isSupported = TextUtils.equals(prevUserDisplayName, senderDisplayName) && TextUtils.equals(avatar, prevAvatar); } } @@ -1731,7 +1824,7 @@ String headerMessage(int position) { * @param contentView the cell view. * @param event the linked event */ - private void manageSelectionMode(final View contentView, final Event event) { + private void manageSelectionMode(final View contentView, final Event event, final int msgType) { final String eventId = event.eventId; boolean isInSelectionMode = (null != mSelectedEventId); @@ -1746,6 +1839,11 @@ private void manageSelectionMode(final View contentView, final Event event) { contentView.findViewById(R.id.messagesAdapter_body_view).setAlpha(alpha); contentView.findViewById(R.id.messagesAdapter_avatars_list).setAlpha(alpha); + View urlsPreviewView = contentView.findViewById(R.id.messagesAdapter_urls_preview_list); + if (null != urlsPreviewView) { + urlsPreviewView.setAlpha(alpha); + } + TextView tsTextView = contentView.findViewById(R.id.messagesAdapter_timestamp); if (isInSelectionMode && isSelected) { tsTextView.setVisibility(View.VISIBLE); @@ -1756,7 +1854,7 @@ private void manageSelectionMode(final View contentView, final Event event) { @Override public void onClick(View v) { if (TextUtils.equals(eventId, mSelectedEventId)) { - onMessageClick(event, getEventText(contentView), contentView.findViewById(R.id.messagesAdapter_action_anchor)); + onMessageClick(event, getEventText(contentView, event, msgType), contentView.findViewById(R.id.messagesAdapter_action_anchor)); } else { onEventTap(eventId); } @@ -1767,7 +1865,7 @@ public void onClick(View v) { @Override public boolean onLongClick(View v) { if (!mIsSearchMode) { - onMessageClick(event, getEventText(contentView), contentView.findViewById(R.id.messagesAdapter_action_anchor)); + onMessageClick(event, getEventText(contentView, event, msgType), contentView.findViewById(R.id.messagesAdapter_action_anchor)); mSelectedEventId = eventId; notifyDataSetChanged(); return true; @@ -1801,14 +1899,19 @@ boolean mergeView(Event event, int position, boolean shouldBeMerged) { * @param contentView the cell view * @return the displayed text. */ - private String getEventText(View contentView) { + private String getEventText(View contentView, Event event, int msgType) { String text = null; if (null != contentView) { - TextView bodyTextView = contentView.findViewById(R.id.messagesAdapter_body); + if ((ROW_TYPE_CODE == msgType) || (ROW_TYPE_TEXT == msgType)) { + final Message message = JsonUtils.toMessage(event.getContent()); + text = message.body; + } else { + TextView bodyTextView = contentView.findViewById(R.id.messagesAdapter_body); - if (null != bodyTextView) { - text = bodyTextView.getText().toString(); + if (null != bodyTextView) { + text = bodyTextView.getText().toString(); + } } } @@ -1822,7 +1925,7 @@ private String getEventText(View contentView) { * @param contentView the main message view * @param position the item position */ - private void addContentViewListeners(final View convertView, final View contentView, final int position) { + private void addContentViewListeners(final View convertView, final View contentView, final int position, final int msgType) { contentView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -1844,7 +1947,7 @@ public boolean onLongClick(View v) { Event event = row.getEvent(); if (!mIsSearchMode) { - onMessageClick(event, getEventText(contentView), convertView.findViewById(R.id.messagesAdapter_action_anchor)); + onMessageClick(event, getEventText(contentView, event, msgType), convertView.findViewById(R.id.messagesAdapter_action_anchor)); mSelectedEventId = event.eventId; notifyDataSetChanged(); return true; @@ -2023,6 +2126,15 @@ public void setReadMarkerListener(final ReadMarkerListener listener) { mReadMarkerListener = listener; } + /** + * Set a image getter + * + * @param imageGetter the image getter + */ + public void setImageGetter(VectorImageGetter imageGetter) { + mHelper.setImageGetter(imageGetter); + } + /** * Animate a read marker view */ @@ -2267,7 +2379,7 @@ private void onMessageClick(final Event event, final String textMsg, final View Message message = JsonUtils.toMessage(event.getContentAsJsonObject()); // share / forward the message - menu.findItem(R.id.ic_action_vector_share).setVisible(true); + menu.findItem(R.id.ic_action_vector_share).setVisible(!mIsRoomEncrypted); menu.findItem(R.id.ic_action_vector_forward).setVisible(true); // save the media in the downloads directory @@ -2467,4 +2579,4 @@ private void checkEventGroupsMerge(MessageRow deletedRow, int position) { } } } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterHelper.java b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterHelper.java index 947a0edbce..3aaaee2884 100755 --- a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterHelper.java +++ b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterHelper.java @@ -33,6 +33,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import com.google.gson.JsonNull; @@ -40,10 +41,16 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.adapters.MessageRow; +import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.data.RoomState; import org.matrix.androidsdk.data.store.IMXStore; +import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.URLPreview; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.rest.model.group.GroupProfile; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.rest.model.ReceiptData; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.util.EventDisplay; @@ -54,20 +61,25 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import im.vector.R; +import im.vector.VectorApp; import im.vector.listeners.IMessagesAdapterActionsListener; import im.vector.util.MatrixLinkMovementMethod; import im.vector.util.MatrixURLSpan; import im.vector.util.PreferencesManager; import im.vector.util.RiotEventDisplay; import im.vector.util.ThemeUtils; +import im.vector.util.VectorImageGetter; import im.vector.util.VectorUtils; import im.vector.view.PillView; +import im.vector.view.UrlPreviewView; import im.vector.widgets.WidgetsManager; /** @@ -76,14 +88,30 @@ class VectorMessagesAdapterHelper { private static final String LOG_TAG = VectorMessagesAdapterHelper.class.getSimpleName(); + /** + * Enable multiline mode, split on
...
and retain those delimiters in + * the returned fenced block. + */ + public static final String START_FENCED_BLOCK = "
";
+    public static final String END_FENCED_BLOCK = "
"; + private static final Pattern FENCED_CODE_BLOCK_PATTERN = Pattern.compile("(?m)(?=
)|(?<=
)"); + private IMessagesAdapterActionsListener mEventsListener; - private final MXSession mSession; + private final Context mContext; + private final MXSession mSession; + private final VectorMessagesAdapter mAdapter; + private Room mRoom = null; + private MatrixLinkMovementMethod mLinkMovementMethod; - VectorMessagesAdapterHelper(Context context, MXSession session) { + private VectorImageGetter mImageGetter; + + + VectorMessagesAdapterHelper(Context context, MXSession session, VectorMessagesAdapter adapter) { mContext = context; mSession = session; + mAdapter = adapter; } /** @@ -95,7 +123,6 @@ void setVectorMessagesAdapterActionsListener(IMessagesAdapterActionsListener lis mEventsListener = listener; } - /** * Define the links movement method * @@ -105,6 +132,15 @@ void setLinkMovementMethod(MatrixLinkMovementMethod method) { mLinkMovementMethod = method; } + /** + * Set the image getter. + * + * @param imageGetter the image getter + */ + void setImageGetter(VectorImageGetter imageGetter) { + mImageGetter = imageGetter; + } + /** * Returns an user display name for an user Id. * @@ -130,12 +166,15 @@ public static String getUserDisplayName(String userId, RoomState roomState) { public void setSenderValue(View convertView, MessageRow row, boolean isMergedView) { // manage sender text TextView senderTextView = convertView.findViewById(R.id.messagesAdapter_sender); + View groupFlairView = convertView.findViewById(R.id.messagesAdapter_flair_groups_list); if (null != senderTextView) { Event event = row.getEvent(); if (isMergedView) { senderTextView.setVisibility(View.GONE); + groupFlairView.setVisibility(View.GONE); + groupFlairView.setTag(null); } else { String eventType = event.getType(); @@ -149,6 +188,8 @@ public void setSenderValue(View convertView, MessageRow row, boolean isMergedVie Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(eventType) || Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(eventType)) { senderTextView.setVisibility(View.GONE); + groupFlairView.setVisibility(View.GONE); + groupFlairView.setTag(null); } else { senderTextView.setVisibility(View.VISIBLE); senderTextView.setText(getUserDisplayName(event.getSender(), row.getRoomState())); @@ -164,8 +205,189 @@ public void onClick(View v) { } } }); + + refreshGroupFlairView(groupFlairView, event); + } + } + } + } + + /** + * Refresh the flairs group view + * + * @param groupFlairView the flairs view + * @param event the event + * @param groupIdsSet the groupids + * @param tag the tag + */ + private void refreshGroupFlairView(final View groupFlairView, final Event event, final Set groupIdsSet, final String tag) { + Log.d(LOG_TAG, "## refreshGroupFlairView () : " + event.sender + " allows flair to " + groupIdsSet); + Log.d(LOG_TAG, "## refreshGroupFlairView () : room related groups " + mRoom.getLiveState().getRelatedGroups()); + + if (!groupIdsSet.isEmpty()) { + // keeps only the intersections + groupIdsSet.retainAll(mRoom.getLiveState().getRelatedGroups()); + } + + Log.d(LOG_TAG, "## refreshGroupFlairView () : group ids to display " + groupIdsSet); + + if (groupIdsSet.isEmpty()) { + groupFlairView.setVisibility(View.GONE); + } else { + + if (!mSession.isAlive()) { + return; + } + + groupFlairView.setVisibility(View.VISIBLE); + + ArrayList imageViews = new ArrayList<>(); + + imageViews.add((ImageView) (groupFlairView.findViewById(R.id.message_avatar_group_1).findViewById(R.id.avatar_img))); + imageViews.add((ImageView) (groupFlairView.findViewById(R.id.message_avatar_group_2).findViewById(R.id.avatar_img))); + imageViews.add((ImageView) (groupFlairView.findViewById(R.id.message_avatar_group_3).findViewById(R.id.avatar_img))); + + TextView moreText = groupFlairView.findViewById(R.id.message_more_than_expected); + + final List groupIds = new ArrayList<>(groupIdsSet); + int index = 0; + int bound = Math.min(groupIds.size(), imageViews.size()); + + for (; index < bound; index++) { + final String groupId = groupIds.get(index); + final ImageView imageView = imageViews.get(index); + + imageView.setVisibility(View.VISIBLE); + + Group group = mSession.getGroupsManager().getGroup(groupId); + + if (null == group) { + group = new Group(groupId); + } + + GroupProfile cachedGroupProfile = mSession.getGroupsManager().getGroupProfile(groupId); + + if (null != cachedGroupProfile) { + Log.d(LOG_TAG, "## refreshGroupFlairView () : profile of " + groupId + " is cached"); + group.setGroupProfile(cachedGroupProfile); + VectorUtils.loadGroupAvatar(mContext, mSession, imageView, group); + } else { + VectorUtils.loadGroupAvatar(mContext, mSession, imageView, group); + + Log.d(LOG_TAG, "## refreshGroupFlairView () : get profile of " + groupId); + + mSession.getGroupsManager().getGroupProfile(groupId, new ApiCallback() { + private void refresh(GroupProfile profile) { + if (TextUtils.equals((String) groupFlairView.getTag(), tag)) { + Group group = new Group(groupId); + group.setGroupProfile(profile); + Log.d(LOG_TAG, "## refreshGroupFlairView () : refresh group avatar " + groupId); + VectorUtils.loadGroupAvatar(mContext, mSession, imageView, group); + } + } + + @Override + public void onSuccess(GroupProfile groupProfile) { + Log.d(LOG_TAG, "## refreshGroupFlairView () : get profile of " + groupId + " succeeded"); + refresh(groupProfile); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupFlairView () : get profile of " + groupId + " failed " + e.getMessage()); + refresh(null); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## refreshGroupFlairView () : get profile of " + groupId + " failed " + e.getMessage()); + refresh(null); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupFlairView () : get profile of " + groupId + " failed " + e.getMessage()); + refresh(null); + } + }); } } + + for (; index < imageViews.size(); index++) { + imageViews.get(index).setVisibility(View.GONE); + } + + moreText.setVisibility((groupIdsSet.size() <= imageViews.size()) ? View.GONE : View.VISIBLE); + moreText.setText("+" + (groupIdsSet.size() - imageViews.size())); + + if (groupIdsSet.size() > 0) { + groupFlairView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != mEventsListener) { + mEventsListener.onGroupFlairClick(event.getSender() + , groupIds); + } + } + }); + } else { + groupFlairView.setOnClickListener(null); + } + } + } + + /** + * Refresh the group flair view + * + * @param groupFlairView the flairs view + * @param event the event + */ + private void refreshGroupFlairView(final View groupFlairView, final Event event) { + final String tag = event.getSender() + "__" + event.eventId; + + if (null == mRoom) { + mRoom = mSession.getDataHandler().getRoom(event.roomId); + } + + // no related groups to this room + if (mRoom.getLiveState().getRelatedGroups().isEmpty()) { + Log.d(LOG_TAG, "## refreshGroupFlairView () : no related group"); + groupFlairView.setVisibility(View.GONE); + return; + } + + groupFlairView.setTag(tag); + + Log.d(LOG_TAG, "## refreshGroupFlairView () : eventId " + event.eventId + " from " + event.sender); + + // cached value first + Set userPublicisedGroups = mSession.getGroupsManager().getUserPublicisedGroups(event.getSender()); + + if (null != userPublicisedGroups) { + refreshGroupFlairView(groupFlairView, event, userPublicisedGroups, tag); + } else { + groupFlairView.setVisibility(View.GONE); + mSession.getGroupsManager().getUserPublicisedGroups(event.getSender(), false, new ApiCallback>() { + @Override + public void onSuccess(Set groupIdsSet) { + refreshGroupFlairView(groupFlairView, event, groupIdsSet, tag); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupFlairView failed " + e.getMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## refreshGroupFlairView failed " + e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupFlairView failed " + e.getMessage()); + } + }); } } @@ -506,7 +728,7 @@ static void setMediaProgressLayout(View convertView, View bodyLayoutView) { } // cache the pills to avoid compute them again - Map mPillsCache = new HashMap<>(); + private Map mPillsDrawableCache = new HashMap<>(); /** * Trap the clicked URL. @@ -523,17 +745,31 @@ private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan int flags = strBuilder.getSpanFlags(span); if (PillView.isPillable(span.getURL())) { - String key = span.getURL() + " " + isHighlighted; - Drawable drawable = mPillsCache.get(key); + final String key = span.getURL() + " " + isHighlighted; + Drawable drawable = mPillsDrawableCache.get(key); if (null == drawable) { - PillView aView = new PillView(mContext); - aView.setText(strBuilder.subSequence(start, end), span.getURL()); + final PillView aView = new PillView(mContext); + aView.initData(strBuilder.subSequence(start, end), span.getURL(), mSession, new PillView.OnUpdateListener() { + @Override + public void onAvatarUpdate() { + // force to compose + aView.setBackgroundResource(android.R.color.transparent); + + // get a drawable from the view + Drawable updatedDrawable = aView.getDrawable(); + mPillsDrawableCache.put(key, updatedDrawable); + // should update only the current cell + // but it might have been recycled + mAdapter.notifyDataSetChanged(); + } + }); aView.setHighlighted(isHighlighted); drawable = aView.getDrawable(); } + if (null != drawable) { - mPillsCache.put(key, drawable); + mPillsDrawableCache.put(key, drawable); ImageSpan imageSpan = new ImageSpan(drawable); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); strBuilder.setSpan(imageSpan, start, end, flags); @@ -553,6 +789,59 @@ public void onClick(View view) { } } + /** + * Determine if the message body contains any code blocks. + * + * @param message the message + * @return true if it contains code blocks + */ + boolean containsFencedCodeBlocks(final Message message) { + return (null != message.formatted_body) && + message.formatted_body.contains(START_FENCED_BLOCK) && + message.formatted_body.contains(END_FENCED_BLOCK); + } + + private Map mCodeBlocksMap = new HashMap<>(); + + /** + * Split the message body with code blocks delimiters. + * + * @param message the message + * @return the split message body + */ + String[] getFencedCodeBlocks(final Message message) { + if (TextUtils.isEmpty(message.formatted_body)) { + return new String[0]; + } + + String[] codeBlocks = mCodeBlocksMap.get(message.formatted_body); + + if (null == codeBlocks) { + codeBlocks = FENCED_CODE_BLOCK_PATTERN.split(message.formatted_body); + mCodeBlocksMap.put(message.formatted_body, codeBlocks); + } + + return codeBlocks; + } + + /** + * Highlight fenced code + * + * @param textView the text view + */ + void highlightFencedCode(final TextView textView) { + // sanity check + if (null == textView) { + return; + } + + textView.setBackgroundColor(ThemeUtils.getColor(mContext, R.attr.markdown_block_background_color)); + + if (null != mLinkMovementMethod) { + textView.setMovementMethod(mLinkMovementMethod); + } + } + /** * Highlight the pattern in the text. * @@ -571,8 +860,8 @@ void highlightPattern(TextView textView, Spannable text, String htmlFormattedTex if (!TextUtils.isEmpty(pattern) && !TextUtils.isEmpty(text) && (text.length() >= pattern.length())) { - String lowerText = text.toString().toLowerCase(); - String lowerPattern = pattern.toLowerCase(); + String lowerText = text.toString().toLowerCase(VectorApp.getApplicationLocale()); + String lowerPattern = pattern.toLowerCase(VectorApp.getApplicationLocale()); int start = 0; int pos = lowerText.indexOf(lowerPattern, start); @@ -597,7 +886,7 @@ void highlightPattern(TextView textView, Spannable text, String htmlFormattedTex // the links are not yet supported by ConsoleHtmlTagHandler // the markdown tables are not properly supported - sequence = Html.fromHtml(htmlFormattedText, null, isCustomizable ? htmlTagHandler : null); + sequence = Html.fromHtml(htmlFormattedText, mImageGetter, isCustomizable ? htmlTagHandler : null); // sanity check if (!TextUtils.isEmpty(sequence)) { @@ -664,7 +953,7 @@ static boolean isDisplayableEvent(Context context, MessageRow row) { if (Event.EVENT_TYPE_MESSAGE.equals(eventType)) { // A message is displayable as long as it has a body Message message = JsonUtils.toMessage(event.getContent()); - return (message.body != null) && (!message.body.equals("")); + return !TextUtils.isEmpty(message.body) || TextUtils.equals(message.msgtype, Message.MSGTYPE_EMOTE); } else if (Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(eventType) || Event.EVENT_TYPE_STATE_ROOM_NAME.equals(eventType)) { EventDisplay display = new RiotEventDisplay(context, event, roomState); @@ -721,13 +1010,12 @@ String getSanitisedHtml(final String html) { return res; } - private static final List mAllowedHTMLTags = Arrays.asList( + private static final Set mAllowedHTMLTags = new HashSet<>(Arrays.asList( "font", // custom to matrix for IRC-style font coloring "del", // for markdown - // deliberately no h1/h2 to stop people shouting. - "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", + "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", "sup", "sub", "nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", - "table", "thead", "caption", "tbody", "tr", "th", "td", "pre"); + "table", "thead", "caption", "tbody", "tr", "th", "td", "pre", "span", "img")); private static final Pattern mHtmlPatter = Pattern.compile("<(\\w+)[^>]*>", Pattern.CASE_INSENSITIVE); @@ -742,7 +1030,7 @@ private static String sanitiseHTML(final String htmlString) { String html = htmlString; Matcher matcher = mHtmlPatter.matcher(htmlString); - ArrayList tagsToRemove = new ArrayList<>(); + HashSet tagsToRemove = new HashSet<>(); while (matcher.find()) { @@ -750,11 +1038,8 @@ private static String sanitiseHTML(final String htmlString) { String tag = htmlString.substring(matcher.start(1), matcher.end(1)); // test if the tag is not allowed - if (mAllowedHTMLTags.indexOf(tag) < 0) { - // add it once - if (tagsToRemove.indexOf(tag) < 0) { - tagsToRemove.add(tag); - } + if (!mAllowedHTMLTags.contains(tag)) { + tagsToRemove.add(tag); } } catch (Exception e) { Log.e(LOG_TAG, "sanitiseHTML failed " + e.getLocalizedMessage()); @@ -762,12 +1047,16 @@ private static String sanitiseHTML(final String htmlString) { } // some tags to remove ? - if (tagsToRemove.size() > 0) { + if (!tagsToRemove.isEmpty()) { // append the tags to remove - String tagsToRemoveString = tagsToRemove.get(0); + String tagsToRemoveString = ""; - for (int i = 1; i < tagsToRemove.size(); i++) { - tagsToRemoveString += "|" + tagsToRemove.get(i); + for (String tag : tagsToRemove) { + if (!tagsToRemoveString.isEmpty()) { + tagsToRemoveString += "|"; + } + + tagsToRemoveString += tag; } html = html.replaceAll("<\\/?(" + tagsToRemoveString + ")[^>]*>", ""); @@ -775,4 +1064,127 @@ private static String sanitiseHTML(final String htmlString) { return html; } + + /* + * ********************************************************************************************* + * Url preview managements + * ********************************************************************************************* + */ + private final Map> mExtractedUrls = new HashMap<>(); + private final Map mUrlsPreview = new HashMap<>(); + private final Set mPendingUrls = new HashSet<>(); + private final Set mDismissedPreviews = new HashSet<>(); + + /** + * Retrieves the webUrl extracted from a text + * + * @param text the text + * @return the web urls list + */ + private List extractWebUrl(String text) { + List list = mExtractedUrls.get(text); + + if (null == list) { + list = new ArrayList<>(); + + Matcher matcher = android.util.Patterns.WEB_URL.matcher(text); + while (matcher.find()) { + try { + String value = text.substring(matcher.start(0), matcher.end(0)); + + if (!list.contains(value)) { + list.add(value); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## extractWebUrl() " + e.getMessage()); + } + } + + mExtractedUrls.put(text, list); + } + + return list; + } + + void manageURLPreviews(final Message message, final View convertView, final String id) { + LinearLayout urlsPreviewLayout = convertView.findViewById(R.id.messagesAdapter_urls_preview_list); + + // sanity checks + if (null == urlsPreviewLayout) { + return; + } + + // + if (TextUtils.isEmpty(message.body)) { + urlsPreviewLayout.setVisibility(View.GONE); + return; + } + + List urls = extractWebUrl(message.body); + + if (urls.isEmpty()) { + urlsPreviewLayout.setVisibility(View.GONE); + return; + } + + // avoid removing items if they are displayed + if (TextUtils.equals((String) urlsPreviewLayout.getTag(), id)) { + // all the urls have been displayed + if (urlsPreviewLayout.getChildCount() == urls.size()) { + return; + } + } + + urlsPreviewLayout.setTag(id); + + // remove url previews + while (urlsPreviewLayout.getChildCount() > 0) { + urlsPreviewLayout.removeViewAt(0); + } + + urlsPreviewLayout.setVisibility(View.VISIBLE); + + for (final String url : urls) { + final String downloadKey = url.hashCode() + "---"; + String displayKey = url + "<----->" + id; + + if (UrlPreviewView.didUrlPreviewDismiss(displayKey)) { + Log.d(LOG_TAG, "## manageURLPreviews() : " + displayKey + " has been dismissed"); + } else if (mPendingUrls.contains(url)) { + // please wait + } else if (!mUrlsPreview.containsKey(downloadKey)) { + mPendingUrls.add(url); + mSession.getEventsApiClient().getURLPreview(url, System.currentTimeMillis(), new ApiCallback() { + @Override + public void onSuccess(URLPreview urlPreview) { + mPendingUrls.remove(url); + + if (!mUrlsPreview.containsKey(downloadKey)) { + mUrlsPreview.put(downloadKey, urlPreview); + mAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onNetworkError(Exception e) { + onSuccess(null); + } + + @Override + public void onMatrixError(MatrixError e) { + onSuccess(null); + } + + @Override + public void onUnexpectedError(Exception e) { + onSuccess(null); + } + }); + } else { + UrlPreviewView previewView = new UrlPreviewView(mContext); + previewView.setUrlPreview(mContext, mSession, mUrlsPreview.get(downloadKey), displayKey); + urlsPreviewLayout.addView(previewView); + } + } + } } diff --git a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterMediasHelper.java b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterMediasHelper.java index 716936a4e6..4935cb6ca0 100755 --- a/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterMediasHelper.java +++ b/vector/src/main/java/im/vector/adapters/VectorMessagesAdapterMediasHelper.java @@ -37,18 +37,21 @@ import org.matrix.androidsdk.listeners.IMXMediaUploadListener; import org.matrix.androidsdk.listeners.MXMediaDownloadListener; import org.matrix.androidsdk.listeners.MXMediaUploadListener; -import org.matrix.androidsdk.rest.model.EncryptedFileInfo; +import org.matrix.androidsdk.rest.model.crypto.EncryptedFileInfo; import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.FileMessage; -import org.matrix.androidsdk.rest.model.ImageInfo; -import org.matrix.androidsdk.rest.model.ImageMessage; +import org.matrix.androidsdk.rest.model.message.FileMessage; +import org.matrix.androidsdk.rest.model.message.ImageInfo; +import org.matrix.androidsdk.rest.model.message.ImageMessage; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; -import org.matrix.androidsdk.rest.model.VideoInfo; -import org.matrix.androidsdk.rest.model.VideoMessage; +import org.matrix.androidsdk.rest.model.message.Message; +import org.matrix.androidsdk.rest.model.message.VideoInfo; +import org.matrix.androidsdk.rest.model.message.VideoMessage; import org.matrix.androidsdk.util.JsonUtils; import org.matrix.androidsdk.util.Log; +import java.util.HashMap; +import java.util.Map; + import im.vector.R; import im.vector.listeners.IMessagesAdapterActionsListener; @@ -168,6 +171,10 @@ public void onUploadComplete(final String uploadId, final String contentUri) { refreshUploadViews(event, uploadStats, uploadProgressLayout); } + // the image / video bitmaps are set to null if the matching URL is not the same + // to avoid flickering + private Map mUrlByBitmapIndex = new HashMap<>(); + /** * Manage the image/video download. * @@ -240,8 +247,13 @@ void managePendingImageVideoDownload(final View convertView, final Event event, ImageView imageView = convertView.findViewById(R.id.messagesAdapter_image); - // reset the bitmap - imageView.setImageBitmap(null); + // reset the bitmap if the url is not the same than before + if ((null == thumbUrl) || !TextUtils.equals(imageView.hashCode() + "", mUrlByBitmapIndex.get(thumbUrl))) { + imageView.setImageBitmap(null); + if (null != thumbUrl) { + mUrlByBitmapIndex.put(thumbUrl, imageView.hashCode() + ""); + } + } RelativeLayout informationLayout = convertView.findViewById(R.id.messagesAdapter_image_layout); final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) informationLayout.getLayoutParams(); @@ -397,12 +409,13 @@ void managePendingImageVideoUpload(final View convertView, final Event event, Me String uploadingUrl; final boolean isUploadingThumbnail; + boolean isUploadingContent = false; if (isVideoMessage) { - uploadingUrl = ((VideoMessage) message).info.thumbnail_url; + uploadingUrl = ((VideoMessage) message).getThumbnailUrl(); isUploadingThumbnail = ((VideoMessage) message).isThumbnailLocalContent(); } else { - uploadingUrl = ((ImageMessage) message).info.thumbnailUrl; + uploadingUrl = ((ImageMessage) message).getThumbnailUrl(); isUploadingThumbnail = ((ImageMessage) message).isThumbnailLocalContent(); } @@ -412,10 +425,14 @@ void managePendingImageVideoUpload(final View convertView, final Event event, Me progress = mSession.getMediasCache().getProgressValueForUploadId(uploadingUrl); } else { if (isVideoMessage) { - uploadingUrl = ((VideoMessage) message).url; + uploadingUrl = ((VideoMessage) message).getUrl(); + isUploadingContent = ((VideoMessage) message).isLocalContent(); + } else { - uploadingUrl = ((ImageMessage) message).url; + uploadingUrl = ((ImageMessage) message).getUrl(); + isUploadingContent = ((ImageMessage) message).isLocalContent(); } + progress = mSession.getMediasCache().getProgressValueForUploadId(uploadingUrl); } @@ -478,11 +495,12 @@ public void onUploadComplete(final String uploadId, final String contentUri) { uploadSpinner.setVisibility(((progress < 0) && event.isSending()) ? View.VISIBLE : View.GONE); refreshUploadViews(event, mSession.getMediasCache().getStatsForUploadId(uploadingUrl), uploadProgressLayout); - if (!isUploadingThumbnail) { + if (isUploadingContent) { progress = 10 + (progress * 90 / 100); - } else { + } else if (isUploadingThumbnail) { progress = (progress * 10 / 100); } + updateUploadProgress(uploadProgressLayout, progress); uploadProgressLayout.setVisibility(((progress >= 0) && event.isSending()) ? View.VISIBLE : View.GONE); } diff --git a/vector/src/main/java/im/vector/adapters/VectorParticipantsAdapter.java b/vector/src/main/java/im/vector/adapters/VectorParticipantsAdapter.java index 429923394a..d2495c42ad 100755 --- a/vector/src/main/java/im/vector/adapters/VectorParticipantsAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorParticipantsAdapter.java @@ -40,7 +40,7 @@ import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.rest.model.RoomMember; -import org.matrix.androidsdk.rest.model.Search.SearchUsersResponse; +import org.matrix.androidsdk.rest.model.search.SearchUsersResponse; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.Log; @@ -55,6 +55,7 @@ import im.vector.Matrix; import im.vector.R; +import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.contacts.Contact; import im.vector.contacts.ContactsManager; @@ -182,7 +183,7 @@ public void setSearchedPattern(String pattern, ParticipantAdapterItem firstEntry if (null == pattern) { pattern = ""; } else { - pattern = pattern.toLowerCase().trim().toLowerCase(); + pattern = pattern.toLowerCase().trim().toLowerCase(VectorApp.getApplicationLocale()); } if (!pattern.equals(mPattern) || TextUtils.isEmpty(mPattern)) { @@ -306,7 +307,7 @@ private void listOtherMembers() { iterator.remove(); } else if (!TextUtils.isEmpty(item.mDisplayName)) { // Add to the display names list - displayNamesList.add(item.mDisplayName.toLowerCase()); + displayNamesList.add(item.mDisplayName.toLowerCase(VectorApp.getApplicationLocale())); } } @@ -433,14 +434,15 @@ public void onSuccess(SearchUsersResponse searchUsersResponse) { mIsOfflineContactsSearch = false; mKnownContactsLimited = (null != searchUsersResponse.limited) ? searchUsersResponse.limited : false; - onKnownContactsSearchEnd(participantItemList, theFirstEntry, searchListener); + + searchAccountKnownContacts(theFirstEntry, participantItemList, false, searchListener); } } private void onError() { if (TextUtils.equals(fPattern, mPattern)) { mIsOfflineContactsSearch = true; - searchAccountKnownContacts(theFirstEntry, searchListener); + searchAccountKnownContacts(theFirstEntry, new ArrayList(), true, searchListener); } } @@ -460,19 +462,19 @@ public void onUnexpectedError(Exception e) { } }); } else { - searchAccountKnownContacts(theFirstEntry, searchListener); + searchAccountKnownContacts(theFirstEntry, new ArrayList(), true, searchListener); } } /** * Search the known contacts from the account known users list. * - * @param theFirstEntry the adapter first entry - * @param searchListener the listener + * @param theFirstEntry the adapter first entry + * @param participantItemList the participants initial list + * @param sortRoomContactsList true to sort the room contacts list + * @param searchListener the listener */ - private void searchAccountKnownContacts(final ParticipantAdapterItem theFirstEntry, final OnParticipantsSearchListener searchListener) { - List participantItemList = new ArrayList<>(); - + private void searchAccountKnownContacts(final ParticipantAdapterItem theFirstEntry, final List participantItemList, final boolean sortRoomContactsList, final OnParticipantsSearchListener searchListener) { // the list is not anymore limited mKnownContactsLimited = false; @@ -490,7 +492,7 @@ public void run() { handler.post(new Runnable() { @Override public void run() { - searchAccountKnownContacts(theFirstEntry, searchListener); + searchAccountKnownContacts(theFirstEntry, participantItemList, sortRoomContactsList, searchListener); } }); } @@ -570,7 +572,7 @@ public void run() { } } - onKnownContactsSearchEnd(participantItemList, theFirstEntry, searchListener); + onKnownContactsSearchEnd(participantItemList, theFirstEntry, sortRoomContactsList, searchListener); } /** @@ -579,9 +581,10 @@ public void run() { * * @param participantItemList the known contacts list * @param theFirstEntry the adapter first entry + * @param sort true to sort participantItemList * @param searchListener the search listener */ - private void onKnownContactsSearchEnd(List participantItemList, final ParticipantAdapterItem theFirstEntry, final OnParticipantsSearchListener searchListener) { + private void onKnownContactsSearchEnd(List participantItemList, final ParticipantAdapterItem theFirstEntry, final boolean sort, final OnParticipantsSearchListener searchListener) { // ensure that the PIDs have been retrieved // it might have failed ContactsManager.getInstance().retrievePids(); @@ -655,7 +658,7 @@ private void onKnownContactsSearchEnd(List participantIt } if (!TextUtils.isEmpty(mPattern)) { - if (roomContactsList.size() > 0) { + if ((roomContactsList.size() > 0) && sort) { Collections.sort(roomContactsList, mSortMethod); } mParticipantsListsList.add(roomContactsList); diff --git a/vector/src/main/java/im/vector/adapters/VectorPublicRoomsAdapter.java b/vector/src/main/java/im/vector/adapters/VectorPublicRoomsAdapter.java index b8c74aa48c..fa2f7d7a14 100644 --- a/vector/src/main/java/im/vector/adapters/VectorPublicRoomsAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorPublicRoomsAdapter.java @@ -29,7 +29,7 @@ import im.vector.util.VectorUtils; import org.matrix.androidsdk.MXSession; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; /** * An adapter which can display m.room.member content. diff --git a/vector/src/main/java/im/vector/adapters/VectorRoomCreationAdapter.java b/vector/src/main/java/im/vector/adapters/VectorRoomCreationAdapter.java index 1543af6bbb..39c753d372 100755 --- a/vector/src/main/java/im/vector/adapters/VectorRoomCreationAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorRoomCreationAdapter.java @@ -37,6 +37,7 @@ import im.vector.Matrix; import im.vector.R; +import im.vector.VectorApp; import im.vector.util.VectorUtils; /** @@ -102,7 +103,7 @@ public void notifyDataSetChanged() { ParticipantAdapterItem item = getItem(i); if (!TextUtils.isEmpty(item.mDisplayName)) { - mDisplayNamesList.add(item.mDisplayName.toLowerCase()); + mDisplayNamesList.add(item.mDisplayName.toLowerCase(VectorApp.getApplicationLocale())); } } } diff --git a/vector/src/main/java/im/vector/adapters/VectorRoomDetailsMembersAdapter.java b/vector/src/main/java/im/vector/adapters/VectorRoomDetailsMembersAdapter.java index f0a7f90a9b..8f3a986864 100644 --- a/vector/src/main/java/im/vector/adapters/VectorRoomDetailsMembersAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorRoomDetailsMembersAdapter.java @@ -37,7 +37,7 @@ import org.matrix.androidsdk.db.MXMediasCache; import org.matrix.androidsdk.rest.model.PowerLevels; import org.matrix.androidsdk.rest.model.RoomMember; -import org.matrix.androidsdk.rest.model.RoomThirdPartyInvite; +import org.matrix.androidsdk.rest.model.pid.RoomThirdPartyInvite; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.Log; @@ -48,6 +48,7 @@ import java.util.HashMap; import im.vector.R; +import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.util.ThemeUtils; import im.vector.util.VectorUtils; @@ -208,7 +209,7 @@ public void setSearchedPattern(String aPattern, final OnRoomMembersSearchListene } else { // new pattern different from previous one? if (!aPattern.trim().equals(mSearchPattern) || aIsRefreshForced) { - mSearchPattern = aPattern.trim().toLowerCase(); + mSearchPattern = aPattern.trim().toLowerCase(VectorApp.getApplicationLocale()); updateRoomMembersDataModel(searchListener); } else { // search pattern is identical, notify listener and exit diff --git a/vector/src/main/java/im/vector/adapters/VectorRoomSummaryAdapter.java b/vector/src/main/java/im/vector/adapters/VectorRoomSummaryAdapter.java index 47dedd2178..9e05a21443 100644 --- a/vector/src/main/java/im/vector/adapters/VectorRoomSummaryAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorRoomSummaryAdapter.java @@ -50,6 +50,7 @@ import im.vector.Matrix; import im.vector.PublicRoomsManager; import im.vector.R; +import im.vector.VectorApp; import im.vector.util.RiotEventDisplay; import im.vector.util.RoomUtils; import im.vector.util.ThemeUtils; @@ -222,7 +223,7 @@ private boolean isMatchedPattern(Room room) { if (!TextUtils.isEmpty(mSearchedPattern)) { String roomName = VectorUtils.getRoomDisplayName(mContext, mMxSession, room); - res = (!TextUtils.isEmpty(roomName) && (roomName.toLowerCase().contains(mSearchedPattern))); + res = (!TextUtils.isEmpty(roomName) && (roomName.toLowerCase(VectorApp.getApplicationLocale()).contains(mSearchedPattern))); } return res; @@ -953,7 +954,7 @@ public void setSearchPattern(String pattern) { String trimmedPattern = pattern; if (null != pattern) { - trimmedPattern = pattern.trim().toLowerCase(); + trimmedPattern = pattern.trim().toLowerCase(VectorApp.getApplicationLocale()); trimmedPattern = TextUtils.getTrimmedLength(trimmedPattern) == 0 ? null : trimmedPattern; } diff --git a/vector/src/main/java/im/vector/adapters/VectorSearchFilesListAdapter.java b/vector/src/main/java/im/vector/adapters/VectorSearchFilesListAdapter.java index 9fb9a64150..896fe3b770 100755 --- a/vector/src/main/java/im/vector/adapters/VectorSearchFilesListAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorSearchFilesListAdapter.java @@ -29,12 +29,12 @@ import org.matrix.androidsdk.adapters.MessageRow; import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.db.MXMediasCache; -import org.matrix.androidsdk.rest.model.EncryptedFileInfo; +import org.matrix.androidsdk.rest.model.crypto.EncryptedFileInfo; import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.FileMessage; -import org.matrix.androidsdk.rest.model.ImageMessage; -import org.matrix.androidsdk.rest.model.Message; -import org.matrix.androidsdk.rest.model.VideoMessage; +import org.matrix.androidsdk.rest.model.message.FileMessage; +import org.matrix.androidsdk.rest.model.message.ImageMessage; +import org.matrix.androidsdk.rest.model.message.Message; +import org.matrix.androidsdk.rest.model.message.VideoMessage; import org.matrix.androidsdk.util.JsonUtils; import im.vector.R; diff --git a/vector/src/main/java/im/vector/adapters/VectorSearchMessagesListAdapter.java b/vector/src/main/java/im/vector/adapters/VectorSearchMessagesListAdapter.java index e1e8f52102..3ee94b8bd5 100755 --- a/vector/src/main/java/im/vector/adapters/VectorSearchMessagesListAdapter.java +++ b/vector/src/main/java/im/vector/adapters/VectorSearchMessagesListAdapter.java @@ -32,6 +32,7 @@ import org.matrix.androidsdk.db.MXMediasCache; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.util.EventDisplay; +import org.matrix.androidsdk.util.Log; import im.vector.R; import im.vector.util.RiotEventDisplay; @@ -41,6 +42,7 @@ * An adapter which display a list of messages found after a search */ public class VectorSearchMessagesListAdapter extends VectorMessagesAdapter { + private static final String LOG_TAG = VectorSearchMessagesListAdapter.class.getSimpleName(); // display the room name in the result view private final boolean mDisplayRoomName; @@ -57,6 +59,7 @@ public VectorSearchMessagesListAdapter(MXSession session, Context context, boole R.layout.adapter_item_vector_search_message_image_video, -1, R.layout.adapter_item_vector_search_message_emoji, + R.layout.adapter_item_vector_message_code, mediasCache); setNotifyOnChange(true); @@ -87,110 +90,113 @@ protected boolean supportMessageRowMerge(MessageRow row) { public View getView(int position, View convertView2, ViewGroup parent) { View convertView = super.getView(position, convertView2, parent); - MessageRow row = getItem(position); - Event event = row.getEvent(); - - // some items are always hidden - convertView.findViewById(R.id.messagesAdapter_avatars_list).setVisibility(View.GONE); - convertView.findViewById(R.id.messagesAdapter_message_separator).setVisibility(View.GONE); - convertView.findViewById(R.id.messagesAdapter_action_image).setVisibility(View.GONE); - convertView.findViewById(R.id.messagesAdapter_top_margin_when_no_room_name).setVisibility(mDisplayRoomName ? View.GONE : View.VISIBLE); - convertView.findViewById(R.id.messagesAdapter_message_header).setVisibility(View.GONE); + try { + MessageRow row = getItem(position); + Event event = row.getEvent(); - Room room = mSession.getDataHandler().getStore().getRoom(event.roomId); + // some items are always hidden + convertView.findViewById(R.id.messagesAdapter_avatars_list).setVisibility(View.GONE); + convertView.findViewById(R.id.messagesAdapter_message_separator).setVisibility(View.GONE); + convertView.findViewById(R.id.messagesAdapter_action_image).setVisibility(View.GONE); + convertView.findViewById(R.id.messagesAdapter_top_margin_when_no_room_name).setVisibility(mDisplayRoomName ? View.GONE : View.VISIBLE); + convertView.findViewById(R.id.messagesAdapter_message_header).setVisibility(View.GONE); - RoomState roomState = row.getRoomState(); + Room room = mSession.getDataHandler().getStore().getRoom(event.roomId); - if (null == roomState) { - roomState = room.getLiveState(); - } + RoomState roomState = row.getRoomState(); + if (null == roomState) { + roomState = room.getLiveState(); + } - // refresh the avatar - ImageView avatarView = convertView.findViewById(R.id.messagesAdapter_roundAvatar).findViewById(R.id.avatar_img); - mHelper.loadMemberAvatar(avatarView, row); + // refresh the avatar + ImageView avatarView = convertView.findViewById(R.id.messagesAdapter_roundAvatar).findViewById(R.id.avatar_img); + mHelper.loadMemberAvatar(avatarView, row); - // display the sender - TextView senderTextView = convertView.findViewById(R.id.messagesAdapter_sender); - if (senderTextView != null) { - senderTextView.setText(VectorMessagesAdapterHelper.getUserDisplayName(event.getSender(), roomState)); - } + // display the sender + TextView senderTextView = convertView.findViewById(R.id.messagesAdapter_sender); + if (senderTextView != null) { + senderTextView.setText(VectorMessagesAdapterHelper.getUserDisplayName(event.getSender(), roomState)); + } - // display the body - TextView bodyTextView = convertView.findViewById(R.id.messagesAdapter_body); - // set the message text - EventDisplay display = new RiotEventDisplay(mContext, event, (null != room) ? room.getLiveState() : null); - CharSequence text = display.getTextualDisplay(); + // display the body + TextView bodyTextView = convertView.findViewById(R.id.messagesAdapter_body); + // set the message text + EventDisplay display = new RiotEventDisplay(mContext, event, (null != room) ? room.getLiveState() : null); + CharSequence text = display.getTextualDisplay(); - if (null == text) { - text = ""; - } + if (null == text) { + text = ""; + } - try { - highlightPattern(bodyTextView, new SpannableString(text), mPattern); - } catch (Exception e) { - // an exception might be triggered with HTML content - // Indeed, the formatting can fail because of the single line display. - // in this case, the formatting is ignored. - bodyTextView.setText(text.toString()); - } + try { + highlightPattern(bodyTextView, new SpannableString(text), mPattern); + } catch (Exception e) { + // an exception might be triggered with HTML content + // Indeed, the formatting can fail because of the single line display. + // in this case, the formatting is ignored. + bodyTextView.setText(text.toString()); + } - // display timestamp - TextView timeTextView = convertView.findViewById(R.id.messagesAdapter_timestamp); - timeTextView.setText(AdapterUtils.tsToString(mContext, event.getOriginServerTs(), true)); + // display timestamp + TextView timeTextView = convertView.findViewById(R.id.messagesAdapter_timestamp); + timeTextView.setText(AdapterUtils.tsToString(mContext, event.getOriginServerTs(), true)); - // display the room name - View roomNameLayout = convertView.findViewById(R.id.messagesAdapter_message_room_name_layout); - roomNameLayout.setVisibility(mDisplayRoomName ? View.VISIBLE : View.GONE); + // display the room name + View roomNameLayout = convertView.findViewById(R.id.messagesAdapter_message_room_name_layout); + roomNameLayout.setVisibility(mDisplayRoomName ? View.VISIBLE : View.GONE); - if (mDisplayRoomName) { - TextView roomTextView = convertView.findViewById(R.id.messagesAdapter_message_room_name_textview); - roomTextView.setText(VectorUtils.getRoomDisplayName(mContext, mSession, room)); - } + if (mDisplayRoomName) { + TextView roomTextView = convertView.findViewById(R.id.messagesAdapter_message_room_name_textview); + roomTextView.setText(VectorUtils.getRoomDisplayName(mContext, mSession, room)); + } - // display the day - View dayLayout = convertView.findViewById(R.id.messagesAdapter_search_message_day_separator); + // display the day + View dayLayout = convertView.findViewById(R.id.messagesAdapter_search_message_day_separator); - // day separator - String headerMessage = headerMessage(position); + // day separator + String headerMessage = headerMessage(position); - if (!TextUtils.isEmpty(headerMessage)) { - dayLayout.setVisibility(View.VISIBLE); + if (!TextUtils.isEmpty(headerMessage)) { + dayLayout.setVisibility(View.VISIBLE); - TextView headerText = convertView.findViewById(R.id.messagesAdapter_message_header_text); - headerText.setText(headerMessage); + TextView headerText = convertView.findViewById(R.id.messagesAdapter_message_header_text); + headerText.setText(headerMessage); - dayLayout.findViewById(R.id.messagesAdapter_message_header_top_margin).setVisibility(View.GONE); - dayLayout.findViewById(R.id.messagesAdapter_message_header_bottom_margin).setVisibility(View.GONE); - } else { - dayLayout.setVisibility(View.GONE); - } + dayLayout.findViewById(R.id.messagesAdapter_message_header_top_margin).setVisibility(View.GONE); + dayLayout.findViewById(R.id.messagesAdapter_message_header_bottom_margin).setVisibility(View.GONE); + } else { + dayLayout.setVisibility(View.GONE); + } - // message separator is only displayed when a message is not the last message in a day section - convertView.findViewById(R.id.messagesAdapter_search_separator_line).setVisibility(!TextUtils.isEmpty(headerMessage(position + 1)) ? View.GONE : View.VISIBLE); + // message separator is only displayed when a message is not the last message in a day section + convertView.findViewById(R.id.messagesAdapter_search_separator_line).setVisibility(!TextUtils.isEmpty(headerMessage(position + 1)) ? View.GONE : View.VISIBLE); - final int fPosition = position; + final int fPosition = position; - convertView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (null != mVectorMessagesAdapterEventsListener) { - mVectorMessagesAdapterEventsListener.onContentClick(fPosition); + convertView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != mVectorMessagesAdapterEventsListener) { + mVectorMessagesAdapterEventsListener.onContentClick(fPosition); + } } - } - }); + }); - convertView.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (null != mVectorMessagesAdapterEventsListener) { - return mVectorMessagesAdapterEventsListener.onContentLongClick(fPosition); - } + convertView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (null != mVectorMessagesAdapterEventsListener) { + return mVectorMessagesAdapterEventsListener.onContentLongClick(fPosition); + } - return false; - } - }); + return false; + } + }); + } catch (Throwable t) { + Log.e(LOG_TAG, "## getView() failed " + t.getMessage()); + } return convertView; } diff --git a/vector/src/main/java/im/vector/car/CarBroadcastReceiver.java b/vector/src/main/java/im/vector/car/CarBroadcastReceiver.java index 6cdf49d3f4..638e8b5973 100755 --- a/vector/src/main/java/im/vector/car/CarBroadcastReceiver.java +++ b/vector/src/main/java/im/vector/car/CarBroadcastReceiver.java @@ -14,10 +14,10 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import im.vector.Matrix; -import im.vector.util.NotificationUtils; +import im.vector.notifications.NotificationUtils; public class CarBroadcastReceiver extends BroadcastReceiver { diff --git a/vector/src/main/java/im/vector/contacts/Contact.java b/vector/src/main/java/im/vector/contacts/Contact.java index 936364ef5d..d29cf177e9 100755 --- a/vector/src/main/java/im/vector/contacts/Contact.java +++ b/vector/src/main/java/im/vector/contacts/Contact.java @@ -303,20 +303,20 @@ public boolean contains(String pattern) { boolean matched = false; if (!TextUtils.isEmpty(mDisplayName)) { - matched = (mDisplayName.toLowerCase().contains(pattern)); + matched = (mDisplayName.toLowerCase(VectorApp.getApplicationLocale()).contains(pattern)); } if (!matched) { for (String email : mEmails) { - matched |= email.toLowerCase().contains(pattern); + matched |= email.toLowerCase(VectorApp.getApplicationLocale()).contains(pattern); } } if (!matched) { for (PhoneNumber pn : mPhoneNumbers) { - matched |= pn.mMsisdnPhoneNumber.toLowerCase().contains(pattern) - || pn.mRawPhoneNumber.toLowerCase().contains(pattern) - || (pn.mE164PhoneNumber != null && pn.mE164PhoneNumber.toLowerCase().contains(pattern)); + matched |= pn.mMsisdnPhoneNumber.toLowerCase(VectorApp.getApplicationLocale()).contains(pattern) + || pn.mRawPhoneNumber.toLowerCase(VectorApp.getApplicationLocale()).contains(pattern) + || (pn.mE164PhoneNumber != null && pn.mE164PhoneNumber.toLowerCase(VectorApp.getApplicationLocale()).contains(pattern)); } } diff --git a/vector/src/main/java/im/vector/contacts/PIDsRetriever.java b/vector/src/main/java/im/vector/contacts/PIDsRetriever.java index 0cb8481714..190d29a741 100755 --- a/vector/src/main/java/im/vector/contacts/PIDsRetriever.java +++ b/vector/src/main/java/im/vector/contacts/PIDsRetriever.java @@ -26,7 +26,7 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import java.util.ArrayList; import java.util.Collection; @@ -37,6 +37,7 @@ import java.util.Set; import im.vector.Matrix; +import im.vector.VectorApp; /** * retrieve the contact matrix IDs @@ -177,7 +178,7 @@ private Set retrieveMatrixIds(List contacts) { * @return true if the matrix Ids have been retrieved */ public void retrieveMatrixIds(final Context context, final List contacts, final boolean localUpdateOnly) { - Log.d(LOG_TAG, String.format("retrieveMatrixIds starts for %d contacts", contacts == null ? 0 : contacts.size())); + Log.d(LOG_TAG, String.format(VectorApp.getApplicationLocale(), "retrieveMatrixIds starts for %d contacts", contacts == null ? 0 : contacts.size())); // sanity checks if ((null == contacts) || (0 == contacts.size())) { if (null != mListener) { diff --git a/vector/src/main/java/im/vector/db/VectorContentProvider.java b/vector/src/main/java/im/vector/db/VectorContentProvider.java index e611f0d470..19eb8abf52 100755 --- a/vector/src/main/java/im/vector/db/VectorContentProvider.java +++ b/vector/src/main/java/im/vector/db/VectorContentProvider.java @@ -22,9 +22,10 @@ import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.util.Log; import android.webkit.MimeTypeMap; +import org.matrix.androidsdk.util.Log; + import java.io.File; import java.io.FileNotFoundException; @@ -97,7 +98,7 @@ public int delete(Uri arg0, String arg1, String[] arg2) { @Override public String getType(Uri arg0) { String type = null; - String extension = MimeTypeMap.getFileExtensionFromUrl(arg0.toString().toLowerCase()); + String extension = MimeTypeMap.getFileExtensionFromUrl(arg0.toString().toLowerCase(VectorApp.getApplicationLocale())); if (extension != null) { MimeTypeMap mime = MimeTypeMap.getSingleton(); type = mime.getMimeTypeFromExtension(extension); diff --git a/vector/src/main/java/im/vector/fragments/AbsHomeFragment.java b/vector/src/main/java/im/vector/fragments/AbsHomeFragment.java index 470fcf1e4f..707f80940d 100644 --- a/vector/src/main/java/im/vector/fragments/AbsHomeFragment.java +++ b/vector/src/main/java/im/vector/fragments/AbsHomeFragment.java @@ -32,6 +32,7 @@ import org.matrix.androidsdk.data.RoomSummary; import org.matrix.androidsdk.data.RoomTag; import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.util.BingRulesManager; import org.matrix.androidsdk.util.Log; @@ -53,7 +54,7 @@ /** * Abstract fragment providing the universal search */ -public abstract class AbsHomeFragment extends Fragment implements AbsAdapter.InvitationListener, AbsAdapter.MoreRoomActionListener, RoomUtils.MoreActionListener { +public abstract class AbsHomeFragment extends Fragment implements AbsAdapter.RoomInvitationListener, AbsAdapter.MoreRoomActionListener, RoomUtils.MoreActionListener { private static final String LOG_TAG = AbsHomeFragment.class.getSimpleName(); private static final String CURRENT_FILTER = "CURRENT_FILTER"; @@ -107,7 +108,9 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mActivity = (VectorHomeActivity) getActivity(); + if (getActivity() instanceof VectorHomeActivity) { + mActivity = (VectorHomeActivity) getActivity(); + } mSession = Matrix.getInstance(getActivity()).getDefaultSession(); if (savedInstanceState != null && savedInstanceState.containsKey(CURRENT_FILTER)) { @@ -119,7 +122,7 @@ public void onActivityCreated(final Bundle savedInstanceState) { @CallSuper public void onResume() { super.onResume(); - if (mPrimaryColor != -1) { + if ((mPrimaryColor != -1) && (null != mActivity)) { mActivity.updateTabStyle(mPrimaryColor, mSecondaryColor != -1 ? mSecondaryColor : mPrimaryColor); } } @@ -171,7 +174,7 @@ public void onPreviewRoom(MXSession session, String roomId) { @Override public void onRejectInvitation(MXSession session, String roomId) { Log.i(LOG_TAG, "onRejectInvitation " + roomId); - mActivity.onRejectInvitation(session, roomId); + mActivity.onRejectInvitation(roomId, null); } @Override @@ -184,9 +187,9 @@ public void onMoreActionClick(View itemView, Room room) { } @Override - public void onToggleRoomNotifications(MXSession session, String roomId) { + public void onUpdateRoomNotificationsState(MXSession session, String roomId, BingRulesManager.RoomNotificationState state) { mActivity.showWaitingView(); - RoomUtils.toggleNotifications(session, roomId, new BingRulesManager.onBingRuleUpdateListener() { + session.getDataHandler().getBingRulesManager().updateRoomNotificationState(roomId, state, new BingRulesManager.onBingRuleUpdateListener() { @Override public void onBingRuleUpdateSuccess() { onRequestDone(null); @@ -249,15 +252,36 @@ public void onLeaveRoom(final MXSession session, final String roomId) { @Override public void onClick(DialogInterface dialog, int which) { if (mActivity != null && !mActivity.isFinishing()) { - mActivity.onRejectInvitation(session, roomId); - if (mOnRoomChangedListener != null) { - mOnRoomChangedListener.onRoomLeft(roomId); - } + mActivity.onRejectInvitation(roomId, new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + if (mOnRoomChangedListener != null) { + mOnRoomChangedListener.onRoomLeft(roomId); + } + } + }); + } + } + }); + } + + @Override + public void onForgetRoom(final MXSession session, final String roomId) { + mActivity.onForgetRoom(roomId, new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + if (mOnRoomChangedListener != null) { + mOnRoomChangedListener.onRoomForgot(roomId); } } }); } + @Override + public void addHomeScreenShortcut(MXSession session, String roomId) { + RoomUtils.addHomeScreenShortcut(getActivity(), session, roomId); + } + /* * ********************************************************************************************* * Public methods @@ -330,6 +354,15 @@ void openRoom(final Room room) { } } + /** + * Manage the fab actions + * + * @return true if the fragment has a dedicated action. + */ + public boolean onFabClick() { + return false; + } + /* * ********************************************************************************************* * Private methods @@ -461,5 +494,7 @@ public interface OnRoomChangedListener { void onToggleDirectChat(final String roomId, final boolean isDirectChat); void onRoomLeft(final String roomId); + + void onRoomForgot(final String roomId); } } diff --git a/vector/src/main/java/im/vector/fragments/GroupDetailsBaseFragment.java b/vector/src/main/java/im/vector/fragments/GroupDetailsBaseFragment.java new file mode 100755 index 0000000000..4d98f7af2e --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/GroupDetailsBaseFragment.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import org.matrix.androidsdk.MXSession; + +import butterknife.ButterKnife; +import butterknife.Unbinder; +import im.vector.Matrix; +import im.vector.activity.VectorGroupDetailsActivity; + +public abstract class GroupDetailsBaseFragment extends Fragment { + private static final String LOG_TAG = GroupDetailsBaseFragment.class.getSimpleName(); + + private static final String CURRENT_FILTER = "CURRENT_FILTER"; + + protected MXSession mSession; + protected VectorGroupDetailsActivity mActivity; + + // Butterknife unbinder + private Unbinder mUnBinder; + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mSession = Matrix.getInstance(getContext()).getDefaultSession(); + mActivity = (VectorGroupDetailsActivity) getActivity(); + + initViews(); + } + + @Override + @CallSuper + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mUnBinder = ButterKnife.bind(this, view); + } + + @Override + @CallSuper + public void onDestroyView() { + super.onDestroyView(); + mUnBinder.unbind(); + } + + @Override + @CallSuper + public void onDetach() { + super.onDetach(); + mActivity = null; + } + + @Override + public void onResume() { + super.onResume(); + + if (null != mActivity) { + // dismiss the keyboard when swiping + final View view = mActivity.getCurrentFocus(); + if (view != null) { + final InputMethodManager inputMethodManager = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + } + /* + * ********************************************************************************************* + * Abstract methods + * ********************************************************************************************* + */ + + protected abstract void initViews(); + public abstract void refreshViews(); +} diff --git a/vector/src/main/java/im/vector/fragments/GroupDetailsHomeFragment.java b/vector/src/main/java/im/vector/fragments/GroupDetailsHomeFragment.java new file mode 100755 index 0000000000..3d52c421cb --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/GroupDetailsHomeFragment.java @@ -0,0 +1,168 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.os.Bundle; + +import android.support.v4.content.ContextCompat; + +import android.text.Html; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.matrix.androidsdk.rest.model.group.Group; + +import butterknife.BindView; +import im.vector.R; +import im.vector.activity.CommonActivityUtils; +import im.vector.util.VectorImageGetter; +import im.vector.util.VectorUtils; + +public class GroupDetailsHomeFragment extends GroupDetailsBaseFragment { + private static final String LOG_TAG = GroupDetailsHomeFragment.class.getSimpleName(); + + @BindView(R.id.group_avatar) + ImageView mGroupAvatar; + + @BindView(R.id.group_name_text_view) + TextView mGroupNameTextView; + + @BindView(R.id.group_topic_text_view) + TextView mGroupTopicTextView; + + @BindView(R.id.group_members_icon_view) + ImageView mGroupMembersIconView; + + @BindView(R.id.group_members_text_view) + TextView mGroupMembersTextView; + + @BindView(R.id.group_rooms_icon_view) + ImageView mGroupRoomsIconView; + + @BindView(R.id.group_rooms_text_view) + TextView mGroupRoomsTextView; + + @BindView(R.id.html_text_view) + TextView mGroupHtmlTextView; + + @BindView(R.id.no_html_text_view) + TextView noLongDescriptionTextView; + + private VectorImageGetter mImageGetter; + + /* + * ********************************************************************************************* + * Fragment lifecycle + * ********************************************************************************************* + */ + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (null == mImageGetter) { + mImageGetter = new VectorImageGetter(mSession); + } + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_group_details_home, container, false); + } + + @Override + public void onPause() { + super.onPause(); + mImageGetter.setListener(null); + } + + @Override + public void onResume() { + super.onResume(); + refreshViews(); + + mImageGetter.setListener(new VectorImageGetter.OnImageDownloadListener() { + @Override + public void onImageDownloaded(String source) { + // invalidate the text + refreshLongDescription(); + } + }); + } + + /* + * ********************************************************************************************* + * UI management + * ********************************************************************************************* + */ + @Override + protected void initViews() { + mGroupMembersIconView.setImageDrawable(CommonActivityUtils.tintDrawableWithColor(ContextCompat.getDrawable(mActivity, R.drawable.riot_tab_groups), mGroupMembersTextView.getCurrentTextColor())); + mGroupRoomsIconView.setImageDrawable(CommonActivityUtils.tintDrawableWithColor(ContextCompat.getDrawable(mActivity, R.drawable.riot_tab_rooms), mGroupMembersTextView.getCurrentTextColor())); + } + + /* + * ********************************************************************************************* + * Data management + * ********************************************************************************************* + */ + + @Override + public void refreshViews() { + Group group = mActivity.getGroup(); + + VectorUtils.loadGroupAvatar(mActivity, mSession, mGroupAvatar, group); + + mGroupNameTextView.setText(group.getDisplayName()); + + mGroupTopicTextView.setText(group.getShortDescription()); + mGroupTopicTextView.setVisibility(TextUtils.isEmpty(mGroupTopicTextView.getText()) ? View.GONE : View.VISIBLE); + + int roomCount = (null != group.getGroupRooms()) ? group.getGroupRooms().getEstimatedRoomCount() : 0; + int memberCount = (null != group.getGroupUsers()) ? group.getGroupUsers().getEstimatedUsersCount() : 1; + + mGroupRoomsTextView.setText((1 == roomCount) ? getString(R.string.group_one_room) : getString(R.string.group_rooms, roomCount)); + mGroupMembersTextView.setText((1 == memberCount) ? getString(R.string.group_one_member) : getString(R.string.group_members, memberCount)); + + if (!TextUtils.isEmpty(group.getLongDescription())) { + mGroupHtmlTextView.setVisibility(View.VISIBLE); + refreshLongDescription(); + noLongDescriptionTextView.setVisibility(View.GONE); + } else { + noLongDescriptionTextView.setVisibility(View.VISIBLE); + mGroupHtmlTextView.setVisibility(View.GONE); + } + } + + /** + * Update the long description text + */ + private void refreshLongDescription() { + if (null != mGroupHtmlTextView) { + Group group = mActivity.getGroup(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + mGroupHtmlTextView.setText(Html.fromHtml(group.getLongDescription(), Html.FROM_HTML_MODE_LEGACY, mImageGetter, null)); + } else { + mGroupHtmlTextView.setText(Html.fromHtml(group.getLongDescription(), mImageGetter, null)); + } + } + } +} diff --git a/vector/src/main/java/im/vector/fragments/GroupDetailsPeopleFragment.java b/vector/src/main/java/im/vector/fragments/GroupDetailsPeopleFragment.java new file mode 100755 index 0000000000..fecfed1ecb --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/GroupDetailsPeopleFragment.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; + +import org.matrix.androidsdk.rest.model.group.GroupUser; + +import butterknife.BindView; +import im.vector.R; + +import im.vector.adapters.GroupDetailsPeopleAdapter; +import im.vector.util.GroupUtils; +import im.vector.view.EmptyViewItemDecoration; +import im.vector.view.SimpleDividerItemDecoration; + +public class GroupDetailsPeopleFragment extends GroupDetailsBaseFragment { + @BindView(R.id.recyclerview) + RecyclerView mRecycler; + + @BindView(R.id.search_view) + SearchView mSearchView; + + private GroupDetailsPeopleAdapter mAdapter; + private String mCurrentFilter; + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_group_details_people, container, false); + } + + @Override + public void onResume() { + super.onResume(); + refreshViews(); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mCurrentFilter = mSearchView.getQuery().toString(); + mAdapter.onFilterDone(mCurrentFilter); + } + /* + * ********************************************************************************************* + * UI management + * ********************************************************************************************* + */ + + /** + * Prepare views + */ + @Override + protected void initViews() { + int margin = (int) getResources().getDimension(R.dimen.item_decoration_left_margin); + mRecycler.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); + mRecycler.addItemDecoration(new SimpleDividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, margin)); + mRecycler.addItemDecoration(new EmptyViewItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, 40, 16, 14)); + mAdapter = new GroupDetailsPeopleAdapter(getActivity(), new GroupDetailsPeopleAdapter.OnSelectUserListener() { + @Override + public void onSelectItem(GroupUser user, int position) { + GroupUtils.openGroupUserPage(mActivity, mSession, user); + } + }); + mRecycler.setAdapter(mAdapter); + + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return true; + } + + @Override + public boolean onQueryTextChange(final String newText) { + if (!TextUtils.equals(mCurrentFilter, newText)) { + mAdapter.getFilter().filter(newText, new Filter.FilterListener() { + @Override + public void onFilterComplete(int count) { + mCurrentFilter = newText; + } + }); + } + return true; + } + }); + + mSearchView.setMaxWidth(Integer.MAX_VALUE); + mSearchView.setQueryHint(getString(R.string.filter_group_members)); + mSearchView.setFocusable(false); + mSearchView.setIconifiedByDefault(false); + mSearchView.clearFocus(); + } + + @Override + public void refreshViews() { + mAdapter.setJoinedGroupUsers(mActivity.getGroup().getGroupUsers().getUsers()); + mAdapter.setInvitedGroupUsers(mActivity.getGroup().getInvitedGroupUsers().getUsers()); + } +} diff --git a/vector/src/main/java/im/vector/fragments/GroupDetailsRoomsFragment.java b/vector/src/main/java/im/vector/fragments/GroupDetailsRoomsFragment.java new file mode 100755 index 0000000000..1e032843f8 --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/GroupDetailsRoomsFragment.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; + +import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.model.group.GroupRoom; + +import butterknife.BindView; +import im.vector.R; +import im.vector.adapters.GroupDetailsRoomsAdapter; +import im.vector.util.GroupUtils; +import im.vector.view.EmptyViewItemDecoration; +import im.vector.view.SimpleDividerItemDecoration; + +public class GroupDetailsRoomsFragment extends GroupDetailsBaseFragment { + @BindView(R.id.recyclerview) + RecyclerView mRecycler; + + @BindView(R.id.search_view) + SearchView mSearchView; + + private GroupDetailsRoomsAdapter mAdapter; + private String mCurrentFilter; + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_group_details_rooms, container, false); + } + + @Override + public void onResume() { + super.onResume(); + refreshViews(); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mCurrentFilter = mSearchView.getQuery().toString(); + mAdapter.onFilterDone(mCurrentFilter); + } + + /* + * ********************************************************************************************* + * UI management + * ********************************************************************************************* + */ + + /** + * Prepare views + */ + @Override + protected void initViews() { + int margin = (int) getResources().getDimension(R.dimen.item_decoration_left_margin); + mRecycler.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); + mRecycler.addItemDecoration(new SimpleDividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, margin)); + mRecycler.addItemDecoration(new EmptyViewItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, 40, 16, 14)); + mAdapter = new GroupDetailsRoomsAdapter(getActivity(), new GroupDetailsRoomsAdapter.OnSelectRoomListener() { + @Override + public void onSelectItem(GroupRoom groupRoom, int position) { + mActivity.showWaitingView(); + GroupUtils.openGroupRoom(mActivity, mSession, groupRoom, new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + mActivity.stopWaitingView(); + } + }); + } + }); + mRecycler.setAdapter(mAdapter); + + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return true; + } + + @Override + public boolean onQueryTextChange(final String newText) { + if (!TextUtils.equals(mCurrentFilter, newText)) { + mAdapter.getFilter().filter(newText, new Filter.FilterListener() { + @Override + public void onFilterComplete(int count) { + mCurrentFilter = newText; + } + }); + } + return true; + } + }); + mSearchView.setMaxWidth(Integer.MAX_VALUE); + mSearchView.setQueryHint(getString(R.string.filter_group_rooms)); + mSearchView.setFocusable(false); + mSearchView.setIconifiedByDefault(false); + mSearchView.clearFocus(); + } + + @Override + public void refreshViews() { + mAdapter.setGroupRooms(mActivity.getGroup().getGroupRooms().getRoomsList()); + } +} diff --git a/vector/src/main/java/im/vector/fragments/GroupsFragment.java b/vector/src/main/java/im/vector/fragments/GroupsFragment.java new file mode 100644 index 0000000000..7db8db0e07 --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/GroupsFragment.java @@ -0,0 +1,469 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Filter; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.data.Room; +import org.matrix.androidsdk.groups.GroupsManager; +import org.matrix.androidsdk.listeners.MXEventListener; +import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.util.Log; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import java.util.List; + +import butterknife.BindView; +import im.vector.R; +import im.vector.activity.CommonActivityUtils; +import im.vector.activity.VectorGroupDetailsActivity; +import im.vector.adapters.AbsAdapter; +import im.vector.adapters.GroupAdapter; +import im.vector.util.ThemeUtils; +import im.vector.util.VectorUtils; +import im.vector.view.EmptyViewItemDecoration; +import im.vector.view.SimpleDividerItemDecoration; + +public class GroupsFragment extends AbsHomeFragment { + private static final String LOG_TAG = GroupsFragment.class.getSimpleName(); + + @BindView(R.id.recyclerview) + RecyclerView mRecycler; + + // groups management + private GroupAdapter mAdapter; + private GroupsManager mGroupsManager; + + // rooms list + private final List mJoinedGroups = new ArrayList<>(); + private final List mInvitedGroups = new ArrayList<>(); + + // refresh when there is a group event + private final MXEventListener mEventListener = new MXEventListener() { + @Override + public void onNewGroupInvitation(String groupId) { + refreshGroups(); + } + + @Override + public void onJoinGroup(String groupId) { + refreshGroups(); + } + + @Override + public void onLeaveGroup(String groupId) { + refreshGroups(); + } + }; + + /* + * ********************************************************************************************* + * Static methods + * ********************************************************************************************* + */ + + public static GroupsFragment newInstance() { + return new GroupsFragment(); + } + + /* + * ********************************************************************************************* + * Fragment lifecycle + * ********************************************************************************************* + */ + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_groups, container, false); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mGroupsManager = mSession.getGroupsManager(); + mPrimaryColor = ContextCompat.getColor(getActivity(), R.color.tab_groups); + mSecondaryColor = ContextCompat.getColor(getActivity(), R.color.tab_groups_secondary); + + initViews(); + + mAdapter.onFilterDone(mCurrentFilter); + } + + @Override + public void onResume() { + super.onResume(); + mSession.getDataHandler().addListener(mEventListener); + mRecycler.addOnScrollListener(mScrollListener); + refreshGroupsAndProfiles(); + } + + @Override + public void onPause() { + super.onPause(); + mSession.getDataHandler().removeListener(mEventListener); + mRecycler.removeOnScrollListener(mScrollListener); + } + + /* + * ********************************************************************************************* + * Abstract methods implementation + * ********************************************************************************************* + */ + + @Override + protected List getRooms() { + return new ArrayList<>(); + } + + @Override + protected void onFilter(String pattern, final OnFilterListener listener) { + mAdapter.getFilter().filter(pattern, new Filter.FilterListener() { + @Override + public void onFilterComplete(int count) { + Log.i(LOG_TAG, "onFilterComplete " + count); + if (listener != null) { + listener.onFilterDone(count); + } + } + }); + } + + @Override + protected void onResetFilter() { + mAdapter.getFilter().filter("", new Filter.FilterListener() { + @Override + public void onFilterComplete(int count) { + Log.i(LOG_TAG, "onResetFilter " + count); + } + }); + } + + /* + * ********************************************************************************************* + * UI management + * ********************************************************************************************* + */ + + private void initViews() { + int margin = (int) getResources().getDimension(R.dimen.item_decoration_left_margin); + mRecycler.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); + mRecycler.addItemDecoration(new SimpleDividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, margin)); + mRecycler.addItemDecoration(new EmptyViewItemDecoration(getActivity(), DividerItemDecoration.VERTICAL, 40, 16, 14)); + + mAdapter = new GroupAdapter(getActivity(), new GroupAdapter.OnGroupSelectItemListener() { + @Override + public void onSelectItem(final Group group, final int position) { + // display it + Intent intent = new Intent(getActivity(), VectorGroupDetailsActivity.class); + intent.putExtra(VectorGroupDetailsActivity.EXTRA_GROUP_ID, group.getGroupId()); + intent.putExtra(VectorGroupDetailsActivity.EXTRA_MATRIX_ID, mSession.getCredentials().userId); + startActivity(intent); + } + + @Override + public boolean onLongPressItem(Group item, int position) { + VectorUtils.copyToClipboard(getActivity(), item.getGroupId()); + return true; + } + }, new AbsAdapter.GroupInvitationListener() { + @Override + public void onJoinGroup(MXSession session, String groupId) { + mActivity.showWaitingView(); + mGroupsManager.joinGroup(groupId, new ApiCallback() { + + private void onDone(String errorMessage) { + if ((null != errorMessage) && (null != getActivity())) { + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); + } + mActivity.stopWaitingView(); + } + + @Override + public void onSuccess(Void info) { + onDone(null); + } + + @Override + public void onNetworkError(Exception e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onDone(e.getLocalizedMessage()); + } + }); + } + + @Override + public void onRejectInvitation(MXSession session, String groupId) { + leaveOrReject(groupId); + } + }, new AbsAdapter.MoreGroupActionListener() { + @Override + public void onMoreActionClick(View itemView, Group group) { + displayGroupPopupMenu(group, itemView); + } + }); + + mRecycler.setAdapter(mAdapter); + } + + /** + * Refresh the groups list + */ + private void refreshGroups() { + mJoinedGroups.clear(); + mJoinedGroups.addAll(mGroupsManager.getJoinedGroups()); + mAdapter.setGroups(mJoinedGroups); + + mInvitedGroups.clear(); + mInvitedGroups.addAll(mGroupsManager.getInvitedGroups()); + mAdapter.setInvitedGroups(mInvitedGroups); + } + + /** + * refresh the groups list and their profiles. + */ + private void refreshGroupsAndProfiles() { + refreshGroups(); + mSession.getGroupsManager().refreshGroupProfiles(new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + if ((null != mActivity) && !mActivity.isFinishing()) { + mAdapter.notifyDataSetChanged(); + } + } + }); + } + + /** + * Leave or reject a group invitation. + * + * @param groupId the group Id + */ + private void leaveOrReject(String groupId) { + mActivity.showWaitingView(); + mGroupsManager.leaveGroup(groupId, new ApiCallback() { + + private void onDone(String errorMessage) { + if ((null != errorMessage) && (null != getActivity())) { + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); + } + mActivity.stopWaitingView(); + } + + @Override + public void onSuccess(Void info) { + onDone(null); + } + + @Override + public void onNetworkError(Exception e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onDone(e.getLocalizedMessage()); + } + }); + } + + @SuppressLint("NewApi") + private void displayGroupPopupMenu(final Group group, final View actionView) { + final Context context = getActivity(); + final PopupMenu popup; + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + popup = new PopupMenu(context, actionView, Gravity.END); + } else { + popup = new PopupMenu(context, actionView); + } + popup.getMenuInflater().inflate(R.menu.vector_home_group_settings, popup.getMenu()); + CommonActivityUtils.tintMenuIcons(popup.getMenu(), ThemeUtils.getColor(context, R.attr.settings_icon_tint_color)); + + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(final MenuItem item) { + switch (item.getItemId()) { + case R.id.ic_action_select_remove_group: { + leaveOrReject(group.getGroupId()); + break; + } + } + return false; + } + }); + + + // force to display the icon + try { + Field[] fields = popup.getClass().getDeclaredFields(); + for (Field field : fields) { + if ("mPopup".equals(field.getName())) { + field.setAccessible(true); + Object menuPopupHelper = field.get(popup); + Class classPopupHelper = Class.forName(menuPopupHelper.getClass().getName()); + Method setForceIcons = classPopupHelper.getMethod("setForceShowIcon", boolean.class); + setForceIcons.invoke(menuPopupHelper, true); + break; + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## displayGroupPopupMenu() : failed " + e.getMessage()); + } + + popup.show(); + } + + @Override + public boolean onFabClick() { + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_create_group, null); + alertDialogBuilder.setView(dialogView); + + final EditText nameEditText = dialogView.findViewById(R.id.community_name_edit_text); + final EditText idEditText = dialogView.findViewById(R.id.community_id_edit_text); + final String hostName = mSession.getHomeServerConfig().getHomeserverUri().getHost(); + TextView hsNameView = dialogView.findViewById(R.id.community_hs_name_text_view); + hsNameView.setText(":" + hostName); + + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setTitle(R.string.create_community) + .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + String localPart = idEditText.getText().toString().trim(); + String groupName = nameEditText.getText().toString().trim(); + + mActivity.showWaitingView(); + + mGroupsManager.createGroup(localPart, groupName, new ApiCallback() { + private void onDone(String errorMessage) { + if (null != getActivity()) { + if (null != errorMessage) { + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show(); + } + + mActivity.stopWaitingView(); + + refreshGroups(); + } + } + + @Override + public void onSuccess(String groupId) { + onDone(null); + } + + @Override + public void onNetworkError(Exception e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onDone(e.getLocalizedMessage()); + } + }); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + // create alert dialog + AlertDialog alertDialog = alertDialogBuilder.create(); + + // show it + alertDialog.show(); + + final Button createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + + idEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + createButton.setEnabled(MXSession.isGroupId("+" + idEditText.getText().toString().trim() + ":" + hostName)); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + createButton.setEnabled(false); + return true; + } +} diff --git a/vector/src/main/java/im/vector/fragments/HomeFragment.java b/vector/src/main/java/im/vector/fragments/HomeFragment.java index 13ab98b08f..b3e0bfd9f3 100644 --- a/vector/src/main/java/im/vector/fragments/HomeFragment.java +++ b/vector/src/main/java/im/vector/fragments/HomeFragment.java @@ -51,7 +51,7 @@ import im.vector.util.RoomUtils; import im.vector.view.HomeSectionView; -public class HomeFragment extends AbsHomeFragment implements HomeRoomAdapter.OnSelectRoomListener { +public class HomeFragment extends AbsHomeFragment implements HomeRoomAdapter.OnSelectRoomListener, AbsHomeFragment.OnRoomChangedListener { private static final String LOG_TAG = HomeFragment.class.getSimpleName(); @BindView(R.id.nested_scrollview) @@ -108,6 +108,8 @@ public void onActivityCreated(final Bundle savedInstanceState) { initViews(); + mOnRoomChangedListener = this; + // Eventually restore the pattern of adapter after orientation change for (HomeSectionView homeSectionView : mHomeSectionViews) { homeSectionView.setCurrentFilter(mCurrentFilter); @@ -171,33 +173,33 @@ private void initViews() { mInvitationsSection.setTitle(R.string.invitations_header); mInvitationsSection.setHideIfEmpty(true); mInvitationsSection.setPlaceholders(null, getString(R.string.no_result_placeholder)); - mInvitationsSection.setupRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false), + mInvitationsSection.setupRoomRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false), R.layout.adapter_item_room_invite, false, this, this, null); // Favourites mFavouritesSection.setTitle(R.string.bottom_action_favourites); mFavouritesSection.setHideIfEmpty(true); mFavouritesSection.setPlaceholders(null, getString(R.string.no_result_placeholder)); - mFavouritesSection.setupRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), + mFavouritesSection.setupRoomRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), R.layout.adapter_item_circular_room_view, true, this, null, null); // People mDirectChatsSection.setTitle(R.string.bottom_action_people); mDirectChatsSection.setPlaceholders(getString(R.string.no_conversation_placeholder), getString(R.string.no_result_placeholder)); - mDirectChatsSection.setupRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), + mDirectChatsSection.setupRoomRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), R.layout.adapter_item_circular_room_view, true, this, null, null); // Rooms mRoomsSection.setTitle(R.string.bottom_action_rooms); mRoomsSection.setPlaceholders(getString(R.string.no_room_placeholder), getString(R.string.no_result_placeholder)); - mRoomsSection.setupRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), + mRoomsSection.setupRoomRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), R.layout.adapter_item_circular_room_view, true, this, null, null); // Low priority mLowPrioritySection.setTitle(R.string.low_priority_header); mLowPrioritySection.setHideIfEmpty(true); mLowPrioritySection.setPlaceholders(null, getString(R.string.no_result_placeholder)); - mLowPrioritySection.setupRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), + mLowPrioritySection.setupRoomRecyclerView(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false), R.layout.adapter_item_circular_room_view, true, this, null, null); mHomeSectionViews = Arrays.asList(mInvitationsSection, mFavouritesSection, mDirectChatsSection, mRoomsSection, mLowPrioritySection); @@ -352,4 +354,24 @@ public void onLongClickRoom(View v, Room room, int position) { RoomUtils.displayPopupMenu(getActivity(), mSession, room, v, isFavorite, isLowPriority, this); } + + /* + * ********************************************************************************************* + * Listeners + * ********************************************************************************************* + */ + + @Override + public void onToggleDirectChat(String roomId, boolean isDirectChat) { + } + + @Override + public void onRoomLeft(String roomId) { + } + + @Override + public void onRoomForgot(String roomId) { + // there is no sync event when a room is forgotten + initData(); + } } diff --git a/vector/src/main/java/im/vector/fragments/PeopleFragment.java b/vector/src/main/java/im/vector/fragments/PeopleFragment.java index a7a8850b31..6aeb732366 100644 --- a/vector/src/main/java/im/vector/fragments/PeopleFragment.java +++ b/vector/src/main/java/im/vector/fragments/PeopleFragment.java @@ -44,7 +44,7 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Search.SearchUsersResponse; +import org.matrix.androidsdk.rest.model.search.SearchUsersResponse; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.Log; @@ -68,7 +68,6 @@ import im.vector.view.SimpleDividerItemDecoration; public class PeopleFragment extends AbsHomeFragment implements ContactsManager.ContactsManagerListener, AbsHomeFragment.OnRoomChangedListener { - private static final String LOG_TAG = PeopleFragment.class.getSimpleName(); private static final String MATRIX_USER_ONLY_PREF_KEY = "MATRIX_USER_ONLY_PREF_KEY"; @@ -610,4 +609,9 @@ public void onToggleDirectChat(String roomId, boolean isDirectChat) { public void onRoomLeft(String roomId) { mAdapter.removeDirectChat(roomId); } + + @Override + public void onRoomForgot(String roomId) { + mAdapter.removeDirectChat(roomId); + } } diff --git a/vector/src/main/java/im/vector/fragments/RoomsFragment.java b/vector/src/main/java/im/vector/fragments/RoomsFragment.java index 5696f8e759..a9a070c3bf 100644 --- a/vector/src/main/java/im/vector/fragments/RoomsFragment.java +++ b/vector/src/main/java/im/vector/fragments/RoomsFragment.java @@ -41,7 +41,7 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.client.EventsRestClient; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import org.matrix.androidsdk.util.Log; import java.util.ArrayList; @@ -63,7 +63,7 @@ import im.vector.view.SimpleDividerItemDecoration; public class RoomsFragment extends AbsHomeFragment implements AbsHomeFragment.OnRoomChangedListener { - private static final String LOG_TAG = PeopleFragment.class.getSimpleName(); + private static final String LOG_TAG = RoomsFragment.class.getSimpleName(); // activity result codes private static final int DIRECTORY_SOURCE_ACTIVITY_REQUEST_CODE = 314; @@ -669,11 +669,15 @@ private void removePublicRoomsListener() { @Override public void onToggleDirectChat(String roomId, boolean isDirectChat) { - } @Override public void onRoomLeft(String roomId) { + } + @Override + public void onRoomForgot(String roomId) { + // there is no sync event when a room is forgotten + refreshRooms(); } } diff --git a/vector/src/main/java/im/vector/fragments/VectorMessageListFragment.java b/vector/src/main/java/im/vector/fragments/VectorMessageListFragment.java index fd0f4e3f18..d04b0193d0 100755 --- a/vector/src/main/java/im/vector/fragments/VectorMessageListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorMessageListFragment.java @@ -54,23 +54,22 @@ import org.matrix.androidsdk.listeners.MXMediaDownloadListener; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; -import org.matrix.androidsdk.rest.model.EncryptedEventContent; -import org.matrix.androidsdk.rest.model.EncryptedFileInfo; +import org.matrix.androidsdk.rest.model.crypto.EncryptedEventContent; +import org.matrix.androidsdk.rest.model.crypto.EncryptedFileInfo; import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.FileMessage; -import org.matrix.androidsdk.rest.model.ImageMessage; +import org.matrix.androidsdk.rest.model.message.FileMessage; +import org.matrix.androidsdk.rest.model.message.ImageMessage; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; -import org.matrix.androidsdk.rest.model.VideoMessage; +import org.matrix.androidsdk.rest.model.message.Message; +import org.matrix.androidsdk.rest.model.message.VideoMessage; import org.matrix.androidsdk.util.JsonUtils; import org.matrix.androidsdk.util.Log; import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import im.vector.Matrix; import im.vector.R; @@ -84,8 +83,10 @@ import im.vector.db.VectorContentProvider; import im.vector.listeners.IMessagesAdapterActionsListener; import im.vector.receiver.VectorUniversalLinkReceiver; +import im.vector.util.PreferencesManager; import im.vector.util.SlidableMediaInfo; import im.vector.util.ThemeUtils; +import im.vector.util.VectorImageGetter; import im.vector.util.VectorUtils; import im.vector.widgets.WidgetsManager; @@ -97,6 +98,8 @@ public interface IListFragmentEventListener { } private static final String TAG_FRAGMENT_RECEIPTS_DIALOG = "TAG_FRAGMENT_RECEIPTS_DIALOG"; + private static final String TAG_FRAGMENT_USER_GROUPS_DIALOG = "TAG_FRAGMENT_USER_GROUPS_DIALOG"; + private IListFragmentEventListener mHostActivityListener; // onMediaAction actions @@ -110,6 +113,8 @@ public interface IListFragmentEventListener { private View mForwardProgressView; private View mMainProgressView; + private VectorImageGetter mVectorImageGetter; + public static VectorMessageListFragment newInstance(String matrixId, String roomId, String eventId, String previewMode, int layoutResId) { VectorMessageListFragment f = new VectorMessageListFragment(); Bundle args = new Bundle(); @@ -146,6 +151,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa ((VectorMessagesAdapter) mAdapter).mIsRoomEncrypted = mRoom.isEncrypted(); } + if (null != mSession) { + mVectorImageGetter = new VectorImageGetter(mSession); + ((VectorMessagesAdapter) mAdapter).setImageGetter(mVectorImageGetter); + } + mMessageListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -202,6 +212,8 @@ public void onPause() { adapter.setVectorMessagesAdapterActionsListener(null); adapter.onPause(); } + + mVectorImageGetter.setListener(null); } @@ -212,6 +224,13 @@ public void onResume() { VectorMessagesAdapter adapter = ((VectorMessagesAdapter) mAdapter); adapter.setVectorMessagesAdapterActionsListener(this); } + + mVectorImageGetter.setListener(new VectorImageGetter.OnImageDownloadListener() { + @Override + public void onImageDownloaded(String source) { + mAdapter.notifyDataSetChanged(); + } + }); } /** @@ -757,64 +776,65 @@ public void onClick(DialogInterface dialog, int which) { */ void onMediaAction(final int menuAction, final String mediaUrl, final String mediaMimeType, final String filename, final EncryptedFileInfo encryptedFileInfo) { MXMediasCache mediasCache = Matrix.getInstance(getActivity()).getMediasCache(); - File file = mediasCache.mediaCacheFile(mediaUrl, mediaMimeType); - // check if the media has already been downloaded - if (null != file) { - // download - if ((menuAction == ACTION_VECTOR_SAVE) || (menuAction == ACTION_VECTOR_OPEN)) { - CommonActivityUtils.saveMediaIntoDownloads(getActivity(), file, filename, mediaMimeType, new SimpleApiCallback() { - @Override - public void onSuccess(String savedMediaPath) { - if (null != savedMediaPath) { - if (menuAction == ACTION_VECTOR_SAVE) { - Toast.makeText(getActivity(), getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); - } else { - CommonActivityUtils.openMedia(getActivity(), savedMediaPath, mediaMimeType); - } - } + if (mediasCache.isMediaCached(mediaUrl, mediaMimeType)) { + mediasCache.createTmpMediaFile(mediaUrl, mediaMimeType, encryptedFileInfo, new SimpleApiCallback() { + @Override + public void onSuccess(File file) { + // sanity check + if (null == file) { + return; } - }); - } else { - // shared / forward - Uri mediaUri = null; - File renamedFile = file; + if ((menuAction == ACTION_VECTOR_SAVE) || (menuAction == ACTION_VECTOR_OPEN)) { + CommonActivityUtils.saveMediaIntoDownloads(getActivity(), file, filename, mediaMimeType, new SimpleApiCallback() { + @Override + public void onSuccess(String savedMediaPath) { + if (null != savedMediaPath) { + if (menuAction == ACTION_VECTOR_SAVE) { + Toast.makeText(getActivity(), getText(R.string.media_slider_saved), Toast.LENGTH_LONG).show(); + } else { + CommonActivityUtils.openMedia(getActivity(), savedMediaPath, mediaMimeType); + } + } + } + }); + } else { + if (null != filename) { + File dstFile = new File(file.getParent(), filename); - if (!TextUtils.isEmpty(filename)) { - try { - InputStream fin = new FileInputStream(file); - String tmpUrl = mediasCache.saveMedia(fin, filename, mediaMimeType); + if (dstFile.exists()) { + dstFile.delete(); + } - if (null != tmpUrl) { - renamedFile = mediasCache.mediaCacheFile(tmpUrl, mediaMimeType); + file.renameTo(dstFile); + file = dstFile; } - } catch (Exception e) { - Log.e(LOG_TAG, "onMediaAction shared / forward failed : " + e.getLocalizedMessage()); - } - } - if (null != renamedFile) { - try { - mediaUri = VectorContentProvider.absolutePathToUri(getActivity(), renamedFile.getAbsolutePath()); - } catch (Exception e) { - Log.e(LOG_TAG, "onMediaAction VectorContentProvider.absolutePathToUri: " + e.getLocalizedMessage()); - } - } + // shared / forward + Uri mediaUri = null; + try { + mediaUri = VectorContentProvider.absolutePathToUri(getActivity(), file.getAbsolutePath()); + } catch (Exception e) { + Log.e(LOG_TAG, "onMediaAction VectorContentProvider.absolutePathToUri: " + e.getMessage()); + } - if (null != mediaUri) { - final Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.setType(mediaMimeType); - sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); - if (menuAction == ACTION_VECTOR_FORWARD) { - CommonActivityUtils.sendFilesTo(getActivity(), sendIntent); - } else { - startActivity(sendIntent); + if (null != mediaUri) { + final Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.setType(mediaMimeType); + sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri); + + if (menuAction == ACTION_VECTOR_FORWARD) { + CommonActivityUtils.sendFilesTo(getActivity(), sendIntent); + } else { + startActivity(sendIntent); + } + } } } - } + }); } else { // else download it final String downloadId = mediasCache.downloadMedia(getActivity().getApplicationContext(), mSession.getHomeServerConfig(), mediaUrl, mediaMimeType, encryptedFileInfo); @@ -854,8 +874,7 @@ public void run() { */ @Override public boolean isDisplayAllEvents() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - return preferences.getBoolean(getString(R.string.settings_key_display_all_events), false); + return PreferencesManager.displayAllEvents(getActivity()); } private void setViewVisibility(View view, int visibility) { @@ -1101,6 +1120,22 @@ public void onMoreReadReceiptClick(String eventId) { } } + @Override + public void onGroupFlairClick(String userId, List groupIds) { + try { + FragmentManager fm = getActivity().getSupportFragmentManager(); + + VectorUserGroupsDialogFragment fragment = (VectorUserGroupsDialogFragment) fm.findFragmentByTag(TAG_FRAGMENT_USER_GROUPS_DIALOG); + if (fragment != null) { + fragment.dismissAllowingStateLoss(); + } + fragment = VectorUserGroupsDialogFragment.newInstance(mSession.getMyUserId(), userId, groupIds); + fragment.show(fm, TAG_FRAGMENT_USER_GROUPS_DIALOG); + } catch (Exception e) { + Log.e(LOG_TAG, "## onGroupFlairClick() failed " + e.getMessage()); + } + } + @Override public void onURLClick(Uri uri) { try { @@ -1173,6 +1208,15 @@ public void onMessageIdClick(String messageId) { } } + @Override + public void onGroupIdClick(String groupId) { + try { + onURLClick(Uri.parse(VectorUtils.getPermalink(groupId, null))); + } catch (Exception e) { + Log.e(LOG_TAG, "onRoomIdClick failed " + e.getLocalizedMessage()); + } + } + private int mInvalidIndexesCount = 0; @Override diff --git a/vector/src/main/java/im/vector/fragments/VectorPublicRoomsListFragment.java b/vector/src/main/java/im/vector/fragments/VectorPublicRoomsListFragment.java index c6b9292fc7..2b811634ff 100755 --- a/vector/src/main/java/im/vector/fragments/VectorPublicRoomsListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorPublicRoomsListFragment.java @@ -36,7 +36,7 @@ import org.matrix.androidsdk.data.RoomPreviewData; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import im.vector.Matrix; import im.vector.PublicRoomsManager; diff --git a/vector/src/main/java/im/vector/fragments/VectorReadReceiptsDialogFragment.java b/vector/src/main/java/im/vector/fragments/VectorReadReceiptsDialogFragment.java index 1517f583f8..3274a881c9 100644 --- a/vector/src/main/java/im/vector/fragments/VectorReadReceiptsDialogFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorReadReceiptsDialogFragment.java @@ -20,7 +20,6 @@ import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.text.TextUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,6 +28,7 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.db.MXMediasCache; +import org.matrix.androidsdk.util.Log; import java.util.ArrayList; diff --git a/vector/src/main/java/im/vector/fragments/VectorRecentsListFragment.java b/vector/src/main/java/im/vector/fragments/VectorRecentsListFragment.java index 8f9499abf8..cca156979b 100644 --- a/vector/src/main/java/im/vector/fragments/VectorRecentsListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorRecentsListFragment.java @@ -742,12 +742,69 @@ public void onClick(DialogInterface dialog, int which) { } @Override - public void onToggleRoomNotifications(MXSession session, String roomId) { + public void onForgetRoom(final MXSession session, final String roomId) { + Room room = session.getDataHandler().getRoom(roomId); + + if (null != room) { + showWaitingView(); + + room.forget(new ApiCallback() { + @Override + public void onSuccess(Void info) { + if (null != getActivity()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // clear any pending notification for this room + EventStreamService.cancelNotificationsForRoomId(mSession.getMyUserId(), roomId); + hideWaitingView(); + } + }); + } + } + + private void onError(final String message) { + if (null != getActivity()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + hideWaitingView(); + Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); + } + }); + } + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }); + } + } + + @Override + public void addHomeScreenShortcut(MXSession session, String roomId) { + RoomUtils.addHomeScreenShortcut(getActivity(), session, roomId); + } + + @Override + public void onUpdateRoomNotificationsState(MXSession session, String roomId, BingRulesManager.RoomNotificationState state) { BingRulesManager bingRulesManager = session.getDataHandler().getBingRulesManager(); showWaitingView(); - bingRulesManager.muteRoomNotifications(roomId, !bingRulesManager.isRoomNotificationsDisabled(roomId), new BingRulesManager.onBingRuleUpdateListener() { + bingRulesManager.updateRoomNotificationState(roomId, state, new BingRulesManager.onBingRuleUpdateListener() { @Override public void onBingRuleUpdateSuccess() { if (null != getActivity()) { @@ -773,7 +830,6 @@ public void run() { } ); } - } }); } diff --git a/vector/src/main/java/im/vector/fragments/VectorRoomDetailsMembersFragment.java b/vector/src/main/java/im/vector/fragments/VectorRoomDetailsMembersFragment.java index 731cd2eb1d..8feca35efe 100755 --- a/vector/src/main/java/im/vector/fragments/VectorRoomDetailsMembersFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorRoomDetailsMembersFragment.java @@ -66,7 +66,6 @@ import java.util.TimerTask; import im.vector.R; -import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.activity.MXCActionBarActivity; import im.vector.activity.VectorMemberDetailsActivity; diff --git a/vector/src/main/java/im/vector/fragments/VectorRoomSettingsFragment.java b/vector/src/main/java/im/vector/fragments/VectorRoomSettingsFragment.java index 58a381191d..e294f0ea76 100755 --- a/vector/src/main/java/im/vector/fragments/VectorRoomSettingsFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorRoomSettingsFragment.java @@ -73,9 +73,10 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.List; import im.vector.Matrix; -import im.vector.R; +import im.vector.R;; import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.activity.VectorMediasPickerActivity; @@ -114,7 +115,7 @@ public class VectorRoomSettingsFragment extends PreferenceFragment implements Sh private static final String PREF_KEY_ROOM_TAG_LIST = "roomTagList"; private static final String PREF_KEY_ROOM_ACCESS_RULES_LIST = "roomAccessRulesList"; private static final String PREF_KEY_ROOM_HISTORY_READABILITY_LIST = "roomReadHistoryRulesList"; - private static final String PREF_KEY_ROOM_MUTE_NOTIFICATIONS_SWITCH = "muteNotificationsSwitch"; + private static final String PREF_KEY_ROOM_NOTIFICATIONS_LIST = "roomNotificationPreference"; private static final String PREF_KEY_ROOM_LEAVE = "roomLeave"; private static final String PREF_KEY_ROOM_INTERNAL_ID = "roomInternalId"; private static final String PREF_KEY_ADDRESSES = "addresses"; @@ -124,12 +125,17 @@ public class VectorRoomSettingsFragment extends PreferenceFragment implements Sh private static final String PREF_KEY_BANNED_DIVIDER = "banned_divider"; private static final String PREF_KEY_ENCRYPTION = "encryptionKey"; + private static final String PREF_KEY_FLAIR = "flair"; + private static final String PREF_KEY_FLAIR_DIVIDER = "flair_divider"; + private static final String ADDRESSES_PREFERENCE_KEY_BASE = "ADDRESSES_PREFERENCE_KEY_BASE"; private static final String NO_LOCAL_ADDRESS_PREFERENCE_KEY = "NO_LOCAL_ADDRESS_PREFERENCE_KEY"; private static final String ADD_ADDRESSES_PREFERENCE_KEY = "ADD_ADDRESSES_PREFERENCE_KEY"; private static final String BANNED_PREFERENCE_KEY_BASE = "BANNED_PREFERENCE_KEY_BASE"; + private static final String FLAIR_PREFERENCE_KEY_BASE = "FLAIR_PREFERENCE_KEY_BASE"; + private static final String UNKNOWN_VALUE = "UNKNOWN_VALUE"; // business code @@ -148,17 +154,20 @@ public class VectorRoomSettingsFragment extends PreferenceFragment implements Sh private PreferenceCategory mBannedMembersSettingsCategory; private PreferenceCategory mBannedMembersSettingsCategoryDivider; + // flair + private PreferenceCategory mFlairSettingsCategory; + // UI elements private RoomAvatarPreference mRoomPhotoAvatar; private EditTextPreference mRoomNameEditTxt; private EditTextPreference mRoomTopicEditTxt; private CheckBoxPreference mRoomDirectoryVisibilitySwitch; - private CheckBoxPreference mRoomMuteNotificationsSwitch; private ListPreference mRoomTagListPreference; private VectorListPreference mRoomAccessRulesListPreference; private ListPreference mRoomHistoryReadabilityRulesListPreference; private View mParentLoadingView; private View mParentFragmentContainerView; + private ListPreference mRoomNotificationsPreference; // disable some updates if there is private final IMXNetworkEventListener mNetworkListener = new IMXNetworkEventListener() { @@ -326,7 +335,6 @@ public void onCreate(Bundle savedInstanceState) { mRoomNameEditTxt = (EditTextPreference) findPreference(PREF_KEY_ROOM_NAME); mRoomTopicEditTxt = (EditTextPreference) findPreference(PREF_KEY_ROOM_TOPIC); mRoomDirectoryVisibilitySwitch = (CheckBoxPreference) findPreference(PREF_KEY_ROOM_DIRECTORY_VISIBILITY_SWITCH); - mRoomMuteNotificationsSwitch = (CheckBoxPreference) findPreference(PREF_KEY_ROOM_MUTE_NOTIFICATIONS_SWITCH); mRoomTagListPreference = (ListPreference) findPreference(PREF_KEY_ROOM_TAG_LIST); mRoomAccessRulesListPreference = (VectorListPreference) findPreference(PREF_KEY_ROOM_ACCESS_RULES_LIST); mRoomHistoryReadabilityRulesListPreference = (ListPreference) findPreference(PREF_KEY_ROOM_HISTORY_READABILITY_LIST); @@ -334,6 +342,8 @@ public void onCreate(Bundle savedInstanceState) { mAdvandceSettingsCategory = (PreferenceCategory) getPreferenceManager().findPreference(PREF_KEY_ADVANCED); mBannedMembersSettingsCategory = (PreferenceCategory) getPreferenceManager().findPreference(PREF_KEY_BANNED); mBannedMembersSettingsCategoryDivider = (PreferenceCategory) getPreferenceManager().findPreference(PREF_KEY_BANNED_DIVIDER); + mFlairSettingsCategory = (PreferenceCategory) getPreferenceManager().findPreference(PREF_KEY_FLAIR); + mRoomNotificationsPreference = (ListPreference) getPreferenceManager().findPreference(PREF_KEY_ROOM_NOTIFICATIONS_LIST); mRoomAccessRulesListPreference.setOnPreferenceWarningIconClickListener(new VectorListPreference.OnPreferenceWarningIconClickListener() { @Override @@ -518,6 +528,7 @@ public void onResume() { updateRoomDirectoryVisibilityAsync(); refreshAddresses(); + refreshFlair(); refreshBannedMembersList(); refreshEndToEnd(); } @@ -698,10 +709,6 @@ private void updatePreferenceAccessFromPowerLevel() { if (null != mRoomDirectoryVisibilitySwitch) mRoomDirectoryVisibilitySwitch.setEnabled(isAdmin && isConnected); - // room notification mute setting: no power condition - if (null != mRoomMuteNotificationsSwitch) - mRoomMuteNotificationsSwitch.setEnabled(isConnected); - // room tagging: no power condition if (null != mRoomTagListPreference) mRoomTagListPreference.setEnabled(isConnected); @@ -713,8 +720,13 @@ private void updatePreferenceAccessFromPowerLevel() { } // room read history: admin only - if (null != mRoomHistoryReadabilityRulesListPreference) + if (null != mRoomHistoryReadabilityRulesListPreference) { mRoomHistoryReadabilityRulesListPreference.setEnabled(isAdmin && isConnected); + } + + if (null != mRoomNotificationsPreference) { + mRoomNotificationsPreference.setEnabled(isConnected); + } } @@ -750,12 +762,6 @@ private void updatePreferenceUiValues() { mRoomTopicEditTxt.setText(value); } - // update the mute notifications preference - if (null != mRoomMuteNotificationsSwitch) { - boolean isChecked = mBingRulesManager.isRoomNotificationsDisabled(mRoom.getRoomId()); - mRoomMuteNotificationsSwitch.setChecked(isChecked); - } - // update room directory visibility // if(null != mRoomDirectoryVisibilitySwitch) { // boolean isRoomPublic = TextUtils.equals(mRoom.getVisibility()/*getLiveState().visibility ou .isPublic()*/, RoomState.DIRECTORY_VISIBILITY_PUBLIC); @@ -812,6 +818,23 @@ private void updatePreferenceUiValues() { } } + if (null != mRoomNotificationsPreference) { + BingRulesManager.RoomNotificationState state = mSession.getDataHandler().getBingRulesManager().getRoomNotificationState(mRoom.getRoomId()); + + if (state == BingRulesManager.RoomNotificationState.ALL_MESSAGES_NOISY) { + value = getString(R.string.room_settings_all_messages_noisy); + } else if (state == BingRulesManager.RoomNotificationState.ALL_MESSAGES) { + value = getString(R.string.room_settings_all_messages); + } else if (state == BingRulesManager.RoomNotificationState.MENTIONS_ONLY) { + value = getString(R.string.room_settings_mention_only); + } else { + value = getString(R.string.room_settings_mute); + } + + mRoomNotificationsPreference.setValue(value); + mRoomNotificationsPreference.setSummary(value); + } + // update the room tag preference if (null != mRoomTagListPreference) { @@ -901,8 +924,8 @@ public void onSharedPreferenceChanged(SharedPreferences aSharedPreferences, Stri onRoomNamePreferenceChanged(); } else if (aKey.equals(PREF_KEY_ROOM_TOPIC)) { onRoomTopicPreferenceChanged(); - } else if (aKey.equals(PREF_KEY_ROOM_MUTE_NOTIFICATIONS_SWITCH)) { - onRoomMuteNotificationsPreferenceChanged(); + } else if (aKey.equals(PREF_KEY_ROOM_NOTIFICATIONS_LIST)) { + onRoomNotificationsPreferenceChanged(); } else if (aKey.equals(PREF_KEY_ROOM_DIRECTORY_VISIBILITY_SWITCH)) { onRoomDirectoryVisibilityPreferenceChanged(); // TBT } else if (aKey.equals(PREF_KEY_ROOM_TAG_LIST)) { @@ -1062,34 +1085,43 @@ private void onRoomDirectoryVisibilityPreferenceChanged() { } /** - * Action when enabling / disabling the rooms notifications. */ - private void onRoomMuteNotificationsPreferenceChanged() { + private void onRoomNotificationsPreferenceChanged() { // sanity check - if ((null == mRoom) || (null == mBingRulesManager) || (null == mRoomMuteNotificationsSwitch)) { + if ((null == mRoom) || (null == mBingRulesManager)) { return; } - // get new and previous values - boolean isNotificationsMuted = mRoomMuteNotificationsSwitch.isChecked(); - boolean previousValue = mBingRulesManager.isRoomNotificationsDisabled(mRoom.getRoomId()); + String value = mRoomNotificationsPreference.getValue(); + BingRulesManager.RoomNotificationState updatedState; + + if (TextUtils.equals(value, getString(R.string.room_settings_all_messages_noisy))) { + updatedState = BingRulesManager.RoomNotificationState.ALL_MESSAGES_NOISY; + } else if (TextUtils.equals(value, getString(R.string.room_settings_all_messages))) { + updatedState = BingRulesManager.RoomNotificationState.ALL_MESSAGES; + } else if (TextUtils.equals(value, getString(R.string.room_settings_mention_only))) { + updatedState = BingRulesManager.RoomNotificationState.MENTIONS_ONLY; + } else { + updatedState = BingRulesManager.RoomNotificationState.MUTE; + } // update only, if values are different - if (isNotificationsMuted != previousValue) { + if (mBingRulesManager.getRoomNotificationState(mRoom.getRoomId()) != updatedState) { displayLoadingView(); - mBingRulesManager.muteRoomNotifications(mRoom.getRoomId(), isNotificationsMuted, new BingRulesManager.onBingRuleUpdateListener() { - @Override - public void onBingRuleUpdateSuccess() { - Log.d(LOG_TAG, "##onRoomMuteNotificationsPreferenceChanged(): update succeed"); - hideLoadingView(UPDATE_UI); - } + mBingRulesManager.updateRoomNotificationState(mRoom.getRoomId(), updatedState, + new BingRulesManager.onBingRuleUpdateListener() { + @Override + public void onBingRuleUpdateSuccess() { + Log.d(LOG_TAG, "##onRoomNotificationsPreferenceChanged(): update succeed"); + hideLoadingView(UPDATE_UI); + } - @Override - public void onBingRuleUpdateFailure(String errorMessage) { - Log.w(LOG_TAG, "##onRoomMuteNotificationsPreferenceChanged(): BingRuleUpdateFailure"); - hideLoadingView(DO_NOT_UPDATE_UI); - } - }); + @Override + public void onBingRuleUpdateFailure(String errorMessage) { + Log.w(LOG_TAG, "##onRoomNotificationsPreferenceChanged(): BingRuleUpdateFailure"); + hideLoadingView(DO_NOT_UPDATE_UI); + } + }); } } @@ -1290,7 +1322,7 @@ private void refreshBannedMembersList() { Collections.sort(bannedMembers, new Comparator() { @Override public int compare(RoomMember m1, RoomMember m2) { - return m1.getUserId().toLowerCase().compareTo(m2.getUserId().toLowerCase()); + return m1.getUserId().toLowerCase(VectorApp.getApplicationLocale()).compareTo(m2.getUserId().toLowerCase(VectorApp.getApplicationLocale())); } }); @@ -1329,6 +1361,151 @@ public boolean onPreferenceClick(Preference preference) { } } + //================================================================================ + // flair management + //================================================================================ + + private final ApiCallback mFlairUpdatesCallback = new ApiCallback() { + @Override + public void onSuccess(Void info) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + hideLoadingView(false); + refreshFlair(); + } + }); + } + + /** + * Error management. + * @param errorMessage the error message + */ + private void onError(final String errorMessage) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); + hideLoadingView(false); + refreshFlair(); + } + }); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }; + + /** + * Tells if the current user can updates the related group aka flairs + * + * @return true if the user is allowed. + */ + private boolean canUpdateFlair() { + boolean canUpdateAliases = false; + + PowerLevels powerLevels = mRoom.getLiveState().getPowerLevels(); + + if (null != powerLevels) { + int powerLevel = powerLevels.getUserPowerLevel(mSession.getMyUserId()); + canUpdateAliases = powerLevel >= powerLevels.minimumPowerLevelForSendingEventAsStateEvent(Event.EVENT_TYPE_STATE_RELATED_GROUPS); + } + + return canUpdateAliases; + } + + /** + * Refresh the flair list + */ + private void refreshFlair() { + final List groups = mRoom.getLiveState().getRelatedGroups(); + Collections.sort(groups, String.CASE_INSENSITIVE_ORDER); + + mFlairSettingsCategory.removeAll(); + + if (!groups.isEmpty()) { + for (final String groupId : groups) { + VectorCustomActionEditTextPreference preference = new VectorCustomActionEditTextPreference(getActivity()); + preference.setTitle(groupId); + preference.setKey(FLAIR_PREFERENCE_KEY_BASE + groupId); + + preference.setOnPreferenceLongClickListener(new VectorCustomActionEditTextPreference.OnPreferenceLongClickListener() { + @Override + public boolean onPreferenceLongClick(Preference preference) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + displayLoadingView(); + mRoom.removeRelatedGroup(groupId, mFlairUpdatesCallback); + } + }); + + return true; + } + }); + mFlairSettingsCategory.addPreference(preference); + } + } else { + VectorCustomActionEditTextPreference preference = new VectorCustomActionEditTextPreference(getActivity()); + preference.setTitle(getString(R.string.room_settings_no_flair)); + preference.setKey(FLAIR_PREFERENCE_KEY_BASE + "no_flair"); + + mFlairSettingsCategory.addPreference(preference); + } + + if (canUpdateFlair()) { + // display the "add addresses" entry + EditTextPreference addAddressPreference = new EditTextPreference(getActivity()); + addAddressPreference.setTitle(R.string.room_settings_add_new_group); + addAddressPreference.setDialogTitle(R.string.room_settings_add_new_group); + addAddressPreference.setKey(FLAIR_PREFERENCE_KEY_BASE + "__add"); + addAddressPreference.setIcon(CommonActivityUtils.tintDrawable(getActivity(), ContextCompat.getDrawable(getActivity(), R.drawable.ic_add_black), R.attr.settings_icon_tint_color)); + + addAddressPreference.setOnPreferenceChangeListener( + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String groupId = ((String) newValue).trim(); + + // ignore empty alias + if (!TextUtils.isEmpty(groupId)) { + if (!MXSession.isGroupId(groupId)) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.room_settings_invalid_group_format_dialog_title); + builder.setMessage(getString(R.string.room_settings_invalid_group_format_dialog_body, groupId)); + builder.setPositiveButton(R.string.ok, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } else if (!groups.contains(groupId)) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + displayLoadingView(); + mRoom.addRelatedGroup(groupId, mFlairUpdatesCallback); + } + }); + } + } + return false; + } + }); + + mFlairSettingsCategory.addPreference(addAddressPreference); + } + } + //================================================================================ // Aliases management //================================================================================ diff --git a/vector/src/main/java/im/vector/fragments/VectorSearchRoomFilesListFragment.java b/vector/src/main/java/im/vector/fragments/VectorSearchRoomFilesListFragment.java index ce4550838e..50b594efe8 100644 --- a/vector/src/main/java/im/vector/fragments/VectorSearchRoomFilesListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorSearchRoomFilesListFragment.java @@ -28,7 +28,7 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.rest.model.TokensChunkResponse; import org.matrix.androidsdk.util.JsonUtils; import org.matrix.androidsdk.util.Log; diff --git a/vector/src/main/java/im/vector/fragments/VectorSearchRoomsFilesListFragment.java b/vector/src/main/java/im/vector/fragments/VectorSearchRoomsFilesListFragment.java index 9431cb7928..2608d81b6d 100644 --- a/vector/src/main/java/im/vector/fragments/VectorSearchRoomsFilesListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorSearchRoomsFilesListFragment.java @@ -27,8 +27,8 @@ import org.matrix.androidsdk.adapters.MessageRow; import org.matrix.androidsdk.adapters.AbstractMessagesAdapter; import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.FileMessage; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.FileMessage; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.util.JsonUtils; import java.util.ArrayList; diff --git a/vector/src/main/java/im/vector/fragments/VectorSearchRoomsListFragment.java b/vector/src/main/java/im/vector/fragments/VectorSearchRoomsListFragment.java index 4c6faa496c..80681c05ae 100644 --- a/vector/src/main/java/im/vector/fragments/VectorSearchRoomsListFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorSearchRoomsListFragment.java @@ -32,7 +32,7 @@ import org.matrix.androidsdk.fragments.MatrixMessageListFragment; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import java.util.List; @@ -43,7 +43,6 @@ import im.vector.activity.VectorPublicRoomsActivity; import im.vector.activity.VectorRoomActivity; import im.vector.adapters.VectorRoomSummaryAdapter; -import im.vector.view.RecentsExpandableListView; public class VectorSearchRoomsListFragment extends VectorRecentsListFragment { // the session diff --git a/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.java b/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.java index e47303ea09..8f319aacbc 100755 --- a/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.java +++ b/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.java @@ -24,6 +24,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.graphics.Typeface; import android.media.RingtoneManager; import android.net.Uri; @@ -38,6 +39,7 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; import android.provider.Settings; import android.support.design.widget.TextInputEditText; import android.support.v4.content.ContextCompat; @@ -57,7 +59,6 @@ import android.widget.TextView; import android.widget.Toast; -import com.google.gson.JsonElement; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; @@ -73,11 +74,13 @@ import org.matrix.androidsdk.listeners.MXMediaUploadListener; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; -import org.matrix.androidsdk.rest.model.DeviceInfo; -import org.matrix.androidsdk.rest.model.DevicesListResponse; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.rest.model.search.SearchGroup; +import org.matrix.androidsdk.rest.model.sync.DeviceInfo; +import org.matrix.androidsdk.rest.model.sync.DevicesListResponse; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.ThirdPartyIdentifier; -import org.matrix.androidsdk.rest.model.ThreePid; +import org.matrix.androidsdk.rest.model.pid.ThirdPartyIdentifier; +import org.matrix.androidsdk.rest.model.pid.ThreePid; import org.matrix.androidsdk.rest.model.bingrules.BingRule; import org.matrix.androidsdk.rest.model.bingrules.BingRuleSet; import org.matrix.androidsdk.util.BingRulesManager; @@ -87,12 +90,14 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Set; import im.vector.Matrix; import im.vector.R; @@ -108,6 +113,8 @@ import im.vector.preference.ProgressBarPreference; import im.vector.preference.UserAvatarPreference; import im.vector.preference.VectorCustomActionEditTextPreference; +import im.vector.preference.VectorGroupPreference; +import im.vector.preference.VectorSwitchPreference; import im.vector.util.PhoneNumberUtils; import im.vector.util.PreferencesManager; import im.vector.util.ThemeUtils; @@ -200,6 +207,8 @@ public void onAccountInfoUpdate(MyUser myUser) { private EditTextPreference mSyncRequestDelayPreference; private PreferenceCategory mLabsCategory; + private PreferenceCategory mGroupsFlairCategory; + // static constructor public static VectorSettingsPreferencesFragment newInstance(String matrixId) { VectorSettingsPreferencesFragment f = new VectorSettingsPreferencesFragment(); @@ -497,6 +506,50 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { } }); + final VectorSwitchPreference urlPreviewPreference = (VectorSwitchPreference)findPreference(PreferencesManager.SETTINGS_SHOW_URL_PREVIEW_KEY); + urlPreviewPreference.setChecked(mSession.isURLPreviewEnabled()); + + urlPreviewPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + + if ((null != newValue) && ((boolean)newValue != mSession.isURLPreviewEnabled())) { + displayLoadingView(); + mSession.setURLPreviewStatus((boolean) newValue, new ApiCallback() { + @Override + public void onSuccess(Void info) { + urlPreviewPreference.setChecked(mSession.isURLPreviewEnabled()); + hideLoadingView(); + } + + private void onError(String errorMessage) { + if (null != getActivity()) { + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); + } + onSuccess(null); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }); + } + + return false; + } + }); + // push rules for (String resourceText : mPushesRuleByResourceId.keySet()) { final Preference preference = findPreference(resourceText); @@ -514,7 +567,7 @@ public boolean onPreferenceChange(Preference preference, Object newValueAsVoid) } }); } else if (preference instanceof BingRulePreference) { - final BingRulePreference bingRulePreference = (BingRulePreference)preference; + final BingRulePreference bingRulePreference = (BingRulePreference) preference; bingRulePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { @@ -614,6 +667,7 @@ public void onThirdPartyUnregistrationFailed() { mCryptographyCategory = (PreferenceCategory) findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY); mCryptographyCategoryDivider = (PreferenceCategory) findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY); mLabsCategory = (PreferenceCategory) findPreference(PreferencesManager.SETTINGS_LABS_PREFERENCE_KEY); + mGroupsFlairCategory = (PreferenceCategory) findPreference(PreferencesManager.SETTINGS_GROUPS_FLAIR_KEY); // preference to start the App info screen, to facilitate App permissions access Preference applicationInfoLInkPref = findPreference(APP_INFO_LINK_PREFERENCE_KEY); @@ -757,6 +811,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { refreshPhoneNumbersList(); refreshIgnoredUsersList(); refreshDevicesList(); + refreshGroupFlairsList(); } @Override @@ -922,11 +977,11 @@ private void refreshDisplay() { if (null != preference) { if (preference instanceof BingRulePreference) { - BingRulePreference bingRulePreference = (BingRulePreference)preference; + BingRulePreference bingRulePreference = (BingRulePreference) preference; bingRulePreference.setEnabled((null != rules) && isConnected); bingRulePreference.setBingRule(mSession.getDataHandler().pushRules().findDefaultRule(mPushesRuleByResourceId.get(resourceText))); } else { - CheckBoxPreference switchPreference = (CheckBoxPreference)preference; + CheckBoxPreference switchPreference = (CheckBoxPreference) preference; if (resourceText.equals(PreferencesManager.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)) { switchPreference.setChecked(gcmMgr.areDeviceNotificationsAllowed()); } else if (resourceText.equals(PreferencesManager.SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY)) { @@ -1298,6 +1353,12 @@ public void onActivityResult(int requestCode, int resultCode, final Intent data) switch (requestCode) { case REQUEST_NOTIFICATION_RINGTONE: { PreferencesManager.setNotificationRingTone(getActivity(), (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)); + + // test if the selected ring tone can be played + if (null == PreferencesManager.getNotificationRingToneName(getActivity())) { + PreferencesManager.setNotificationRingTone(getActivity(), PreferencesManager.getNotificationRingTone(getActivity())); + } + refreshNotificationRingTone(); break; } @@ -1388,7 +1449,7 @@ private void refreshPreferences() { for (String resourceText : mPushesRuleByResourceId.keySet()) { Preference preference = findPreference(resourceText); - if ((null != preference) && (preference instanceof CheckBoxPreference)) { + if ((null != preference) && (preference instanceof CheckBoxPreference)) { String ruleId = mPushesRuleByResourceId.get(resourceText); BingRule rule = mBingRuleSet.findDefaultRule(ruleId); @@ -1406,7 +1467,7 @@ else if (isEnabled) { isEnabled = false; } else if (1 == actions.size()) { try { - isEnabled = !TextUtils.equals((String)(actions.get(0)), BingRule.ACTION_DONT_NOTIFY); + isEnabled = !TextUtils.equals((String) (actions.get(0)), BingRule.ACTION_DONT_NOTIFY); } catch (Exception e) { Log.e(LOG_TAG, "## refreshPreferences failed " + e.getMessage()); } @@ -1428,7 +1489,7 @@ else if (isEnabled) { * @param preferenceSummary the displayed 3pid */ private void displayDelete3PIDConfirmationDialog(final ThirdPartyIdentifier pid, final CharSequence preferenceSummary) { - final String mediumFriendlyName = ThreePid.getMediumFriendlyName(pid.medium, getActivity()).toLowerCase(); + final String mediumFriendlyName = ThreePid.getMediumFriendlyName(pid.medium, getActivity()).toLowerCase(VectorApp.getApplicationLocale()); final String dialogMessage = getString(R.string.settings_delete_threepid_confirmation, mediumFriendlyName, preferenceSummary); new AlertDialog.Builder(getActivity()) @@ -1495,7 +1556,7 @@ private void refreshIgnoredUsersList() { Collections.sort(ignoredUsersList, new Comparator() { @Override public int compare(String u1, String u2) { - return u1.toLowerCase().compareTo(u2.toLowerCase()); + return u1.toLowerCase(VectorApp.getApplicationLocale()).compareTo(u2.toLowerCase(VectorApp.getApplicationLocale())); } }); @@ -2915,5 +2976,134 @@ public void onUnexpectedError(Exception e) { } } + //============================================================================================================== + // Group flairs management + //============================================================================================================== + + /** + * Force the refresh of the devices list.
+ * The devices list is the list of the devices where the user as looged in. + * It can be any mobile device, as any browser. + */ + private void refreshGroupFlairsList() { + if (null != mSession) { + // display a spinner while refreshing + if (0 == mGroupsFlairCategory.getPreferenceCount()) { + ProgressBarPreference preference = new ProgressBarPreference(getActivity()); + mGroupsFlairCategory.addPreference(preference); + } + + mSession.getGroupsManager().getUserPublicisedGroups(mSession.getMyUserId(), true, new ApiCallback>() { + @Override + public void onSuccess(Set publicisedGroups) { + buildGroupsList(publicisedGroups); + } + + @Override + public void onNetworkError(Exception e) { + // NOP + } + @Override + public void onMatrixError(MatrixError e) { + // NOP + } + + @Override + public void onUnexpectedError(Exception e) { + // NOP + } + }); + } + } + + // current publicised group list + private Set mPublicisedGroups = null; + + /** + * Build the groups list. + * + * @param publicisedGroups the publicised groups list. + */ + private void buildGroupsList(final Set publicisedGroups) { + boolean isNewList = true; + + if ((null != mPublicisedGroups) && (mPublicisedGroups.size() == publicisedGroups.size())) { + isNewList = !mPublicisedGroups.containsAll(publicisedGroups); + } + + if (isNewList) { + List joinedGroups = new ArrayList<>(mSession.getGroupsManager().getJoinedGroups()); + Collections.sort(joinedGroups, Group.mGroupsComparator); + + int prefIndex = 0; + mPublicisedGroups = publicisedGroups; + + // clear everything + mGroupsFlairCategory.removeAll(); + + for (final Group group : joinedGroups) { + final VectorGroupPreference vectorGroupPreference = new VectorGroupPreference(getActivity()); + vectorGroupPreference.setKey(DEVICES_PREFERENCE_KEY_BASE + prefIndex); + prefIndex++; + + vectorGroupPreference.setGroup(group, mSession); + vectorGroupPreference.setTitle(group.getDisplayName()); + vectorGroupPreference.setSummary(group.getGroupId()); + + vectorGroupPreference.setChecked(publicisedGroups.contains(group.getGroupId())); + mGroupsFlairCategory.addPreference(vectorGroupPreference); + + vectorGroupPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValueAsVoid) { + if (newValueAsVoid instanceof Boolean) { + final boolean newValue = (boolean) newValueAsVoid; + boolean isFlaired = mPublicisedGroups.contains(group.getGroupId()); + + if (newValue != isFlaired) { + displayLoadingView(); + mSession.getGroupsManager().updateGroupPublicity(group.getGroupId(), newValue, new ApiCallback() { + @Override + public void onSuccess(Void info) { + hideLoadingView(); + if (newValue) { + mPublicisedGroups.add(group.getGroupId()); + } else { + mPublicisedGroups.remove(group.getGroupId()); + } + } + + private void onError() { + hideLoadingView(); + // restore default value + vectorGroupPreference.setChecked(publicisedGroups.contains(group.getGroupId())); + } + + @Override + public void onNetworkError(Exception e) { + onError(); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(); + } + }); + } + } + return true; + } + }); + + } + + refreshCryptographyPreference(mMyDeviceInfo); + } + } } diff --git a/vector/src/main/java/im/vector/fragments/VectorUserGroupsDialogFragment.java b/vector/src/main/java/im/vector/fragments/VectorUserGroupsDialogFragment.java new file mode 100644 index 0000000000..5c0604cd3f --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/VectorUserGroupsDialogFragment.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import im.vector.Matrix; +import im.vector.R; +import im.vector.adapters.VectorGroupsListAdapter; + +/** + * A dialog fragment showing the group ids list + */ +public class VectorUserGroupsDialogFragment extends DialogFragment { + private static final String LOG_TAG = VectorUserGroupsDialogFragment.class.getSimpleName(); + + private static final String ARG_SESSION_ID = "ARG_SESSION_ID"; + private static final String ARG_USER_ID = "ARG_USER_ID"; + private static final String ARG_GROUPS_ID = "ARG_GROUPS_ID"; + + public static VectorUserGroupsDialogFragment newInstance(String sessionId, String userId, List groupIds) { + VectorUserGroupsDialogFragment f = new VectorUserGroupsDialogFragment(); + Bundle args = new Bundle(); + args.putString(ARG_SESSION_ID, sessionId); + args.putString(ARG_USER_ID, userId); + args.putStringArrayList(ARG_GROUPS_ID, new ArrayList<>(groupIds)); + f.setArguments(args); + return f; + } + + private MXSession mSession; + private String mUserId; + private ArrayList mGroupIds; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSession = Matrix.getInstance(getContext()).getSession(getArguments().getString(ARG_SESSION_ID)); + mUserId = getArguments().getString(ARG_USER_ID); + mGroupIds = getArguments().getStringArrayList(ARG_GROUPS_ID); + + // sanity check + if ((mSession == null) || TextUtils.isEmpty(mUserId)) { + Log.e(LOG_TAG, "## onCreate() : invalid parameters"); + dismiss(); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View v = inflater.inflate(R.layout.fragment_dialog_groups_list, container, false); + ListView listView = v.findViewById(R.id.listView_groups); + + final VectorGroupsListAdapter adapter = new VectorGroupsListAdapter(getActivity(), R.layout.adapter_item_group_view, mSession); + adapter.addAll(mGroupIds); + listView.setAdapter(adapter); + + return v; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog d = super.onCreateDialog(savedInstanceState); + d.setTitle(getString(R.string.groups_list)); + return d; + } +} diff --git a/vector/src/main/java/im/vector/gcm/GcmRegistrationManager.java b/vector/src/main/java/im/vector/gcm/GcmRegistrationManager.java index 6b8de7c922..244e3d0c5c 100755 --- a/vector/src/main/java/im/vector/gcm/GcmRegistrationManager.java +++ b/vector/src/main/java/im/vector/gcm/GcmRegistrationManager.java @@ -20,13 +20,16 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; +import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; +import org.matrix.androidsdk.HomeServerConnectionConfig; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.client.PushersRestClient; import org.matrix.androidsdk.util.Log; import org.matrix.androidsdk.MXSession; @@ -42,7 +45,9 @@ import im.vector.util.PreferencesManager; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Timer; import java.util.TimerTask; @@ -133,8 +138,8 @@ private enum RegistrationState { // 3 states : null not initialized (retrieved by flavor) private static Boolean mUseGCM; - // - private boolean mLastBatteryOptimizationStatus; + // pusher rest client + private Map mPushersRestClients = new HashMap<>(); /** * Constructor @@ -173,10 +178,40 @@ public void onNetworkConnectionUpdate(boolean isConnected) { }); mRegistrationState = getStoredRegistrationState(); - mLastBatteryOptimizationStatus = PreferencesManager.canStartBackgroundService(mContext); mRegistrationToken = getStoredRegistrationToken(); } + /** + * Retrieves the pushers rest client. + * + * @param session the session + * @return the pushers rest client. + */ + private PushersRestClient getPushersRestClient(MXSession session) { + PushersRestClient pushersRestClient = mPushersRestClients.get(session.getMyUserId()); + + if (null == pushersRestClient) { + // pusher uses a custom server + if (!TextUtils.isEmpty(mContext.getString(R.string.push_server_url))) { + try { + HomeServerConnectionConfig hsConfig = new HomeServerConnectionConfig(Uri.parse(mContext.getString(R.string.push_server_url))); + hsConfig.setCredentials(session.getCredentials()); + pushersRestClient = new PushersRestClient(hsConfig); + } catch (Exception e) { + Log.e(LOG_TAG, "## getPushersRestClient() failed " + e.getMessage()); + } + } + + if (null == pushersRestClient) { + pushersRestClient = session.getPushersRestClient(); + } + + mPushersRestClients.put(session.getMyUserId(), pushersRestClient); + } + + return pushersRestClient; + } + /** * Check if the GCM registration has been broken with a new token ID. * The GCM could have cleared it (onTokenRefresh). @@ -496,8 +531,7 @@ public void onThirdPartyUnregistrationFailed() { * @return true if the registration was done with event Id only */ public void onAppResume() { - if ((mRegistrationState == RegistrationState.SERVER_REGISTERED) && - (mLastBatteryOptimizationStatus != PreferencesManager.canStartBackgroundService(mContext))) { + if (mRegistrationState == RegistrationState.SERVER_REGISTERED) { Log.d(LOG_TAG, "## onAppResume() : force the GCM registration"); forceSessionsRegistration(new ThirdPartyRegistrationListener() { @@ -558,9 +592,9 @@ private void registerToThirdPartyServer(final MXSession session, final boolean a Log.d(LOG_TAG, "registerToThirdPartyServer of " + session.getMyUserId()); - boolean eventIdOnlyPushes = isBackgroundSyncAllowed() && PreferencesManager.canStartBackgroundService(mContext); + boolean eventIdOnlyPushes = isBackgroundSyncAllowed(); - session.getPushersRestClient() + getPushersRestClient(session) .addHttpPusher(mRegistrationToken, DEFAULT_PUSHER_APP_ID, computePushTag(session), mPusherLang, mPusherAppName, mBasePusherDeviceName, DEFAULT_PUSHER_URL, append, eventIdOnlyPushes, new ApiCallback() { @@ -633,7 +667,7 @@ public void onUnexpectedError(Exception e) { */ public void refreshPushersList(List sessions, final ApiCallback callback) { if ((null != sessions) && (sessions.size() > 0)) { - sessions.get(0).getPushersRestClient().getPushers(new ApiCallback() { + getPushersRestClient(sessions.get(0)).getPushers(new ApiCallback() { @Override public void onSuccess(PushersResponse pushersResponse) { @@ -880,9 +914,10 @@ public void onThirdPartyUnregistrationFailed() { * @param callback the asynchronous callback */ public void unregister(final MXSession session, final Pusher pusher, final ApiCallback callback) { - session.getPushersRestClient().removeHttpPusher(pusher.pushkey, pusher.appId, pusher.profileTag, pusher.lang, pusher.appDisplayName, pusher.deviceDisplayName, pusher.data.get("url"), new ApiCallback() { + getPushersRestClient(session).removeHttpPusher(pusher.pushkey, pusher.appId, pusher.profileTag, pusher.lang, pusher.appDisplayName, pusher.deviceDisplayName, pusher.data.get("url"), new ApiCallback() { @Override public void onSuccess(Void info) { + mPushersRestClients.remove(session.getMyUserId()); refreshPushersList(new ArrayList<>(Matrix.getInstance(mContext).getSessions()), callback); } @@ -896,6 +931,7 @@ public void onNetworkError(Exception e) { @Override public void onMatrixError(MatrixError e) { if (e.mStatus == 404) { + mPushersRestClients.remove(session.getMyUserId()); // httpPusher is not available on server side anymore so assume the removal was successful onSuccess(null); return; @@ -923,7 +959,7 @@ public void onUnexpectedError(Exception e) { public void unregister(final MXSession session, final ThirdPartyRegistrationListener listener) { Log.d(LOG_TAG, "unregister " + session.getMyUserId()); - session.getPushersRestClient() + getPushersRestClient(session) .removeHttpPusher(mRegistrationToken, DEFAULT_PUSHER_APP_ID, computePushTag(session), mPusherLang, mPusherAppName, mBasePusherDeviceName, DEFAULT_PUSHER_URL, new ApiCallback() { @@ -1128,7 +1164,7 @@ public void setBackgroundSyncAllowed(boolean isAllowed) { * @return the sync timeout in ms. */ public int getBackgroundSyncTimeOut() { - return getGcmSharedPreferences().getInt(PREFS_SYNC_TIMEOUT, 30000); + return getGcmSharedPreferences().getInt(PREFS_SYNC_TIMEOUT, 6000); } /** @@ -1146,10 +1182,10 @@ public void setBackgroundSyncTimeOut(int syncDelay) { * @return the delay between two syncs in ms. */ public int getBackgroundSyncDelay() { - // on fdroid version, the default sync delay is about 10 minutes + // on fdroid version, the default sync delay is about 1 minutes // set a large value because many users don't know it can be defined from the settings page if ((null == mRegistrationToken) && (null == getStoredRegistrationToken()) && !getGcmSharedPreferences().contains(PREFS_SYNC_DELAY)) { - return 10 * 60 * 1000; + return 60 * 1000; } else { int currentValue = 0; MXSession session = Matrix.getInstance(mContext).getDefaultSession(); diff --git a/vector/src/main/java/im/vector/listeners/IMessagesAdapterActionsListener.java b/vector/src/main/java/im/vector/listeners/IMessagesAdapterActionsListener.java index 1f81493279..0a36786640 100755 --- a/vector/src/main/java/im/vector/listeners/IMessagesAdapterActionsListener.java +++ b/vector/src/main/java/im/vector/listeners/IMessagesAdapterActionsListener.java @@ -21,6 +21,8 @@ import org.matrix.androidsdk.crypto.data.MXDeviceInfo; import org.matrix.androidsdk.rest.model.Event; +import java.util.List; + /** * Actions listeners */ @@ -92,6 +94,14 @@ public interface IMessagesAdapterActionsListener { */ void onMoreReadReceiptClick(String eventId); + /** + * Define the action to perform when the group flairs is clicked. + * + * @param userId the user id + * @param groupIds the group ids list + */ + void onGroupFlairClick(String userId, List groupIds); + /** * An url has been clicked in a message text. * @@ -135,6 +145,13 @@ public interface IMessagesAdapterActionsListener { */ void onMessageIdClick(String messageId); + /** + * A group id has been clicked in a message body. + * + * @param groupId the group id. + */ + void onGroupIdClick(String groupId); + /** * The required indexes are not anymore valid. */ diff --git a/vector/src/main/java/im/vector/util/NotificationUtils.java b/vector/src/main/java/im/vector/notifications/NotificationUtils.java similarity index 62% rename from vector/src/main/java/im/vector/util/NotificationUtils.java rename to vector/src/main/java/im/vector/notifications/NotificationUtils.java index 88c371bbfd..7824c14b66 100755 --- a/vector/src/main/java/im/vector/util/NotificationUtils.java +++ b/vector/src/main/java/im/vector/notifications/NotificationUtils.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package im.vector.util; +package im.vector.notifications; import android.annotation.SuppressLint; import android.app.Notification; @@ -38,16 +38,7 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.style.StyleSpan; -import android.widget.ImageView; - -import org.matrix.androidsdk.MXSession; -import org.matrix.androidsdk.data.Room; -import org.matrix.androidsdk.data.store.IMXStore; -import org.matrix.androidsdk.rest.model.Event; -import org.matrix.androidsdk.rest.model.RoomMember; -import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.rest.model.bingrules.BingRule; -import org.matrix.androidsdk.util.EventDisplay; import org.matrix.androidsdk.util.Log; import im.vector.Matrix; @@ -60,11 +51,8 @@ import im.vector.activity.VectorHomeActivity; import im.vector.activity.VectorRoomActivity; import im.vector.receiver.DismissNotificationReceiver; +import im.vector.util.PreferencesManager; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Random; @@ -82,37 +70,6 @@ public class NotificationUtils { public static final String ACTION_MESSAGE_REPLY = "ACTION_MESSAGE_REPLY"; public static final String EXTRA_ROOM_ID = "EXTRA_ROOM_ID"; - /** - * Retrieve the room name. - * - * @param session the session - * @param room the room - * @param event the event - * @return the room name - */ - public static String getRoomName(Context context, MXSession session, Room room, Event event) { - String roomName = VectorUtils.getRoomDisplayName(context, session, room); - - // avoid displaying the room Id - // try to find the sender display name - if (TextUtils.equals(roomName, room.getRoomId())) { - roomName = room.getName(session.getMyUserId()); - - // avoid room Id as name - if (TextUtils.equals(roomName, room.getRoomId()) && (null != event)) { - User user = session.getDataHandler().getStore().getUser(event.sender); - - if (null != user) { - roomName = user.displayname; - } else { - roomName = event.sender; - } - } - } - - return roomName; - } - // on devices >= android O, we need to define a channel for each notifications public static final String LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"; @@ -357,159 +314,41 @@ else if (width > height) { return resizedBitmap; } - /** - * This class manages the notification display. - * It contains the message to display and its timestamp - */ - static class NotificationDisplay { - final long mEventTs; - final SpannableString mMessage; - - NotificationDisplay(long ts, SpannableString message) { - mEventTs = ts; - mMessage = message; - } - } - - /** - * NotificationDisplay comparator - */ - private static final Comparator mNotificationDisplaySort = new Comparator() { - @Override - public int compare(NotificationDisplay lhs, NotificationDisplay rhs) { - long t0 = lhs.mEventTs; - long t1 = rhs.mEventTs; - - if (t0 > t1) { - return -1; - } else if (t0 < t1) { - return +1; - } - return 0; - } - }; - - /** - * Define a notified event - * i.e the matched bing rules - */ - public static class NotifiedEvent { - public final BingRule mBingRule; - public final String mRoomId; - public final String mEventId; - public final long mOriginServerTs; - - public NotifiedEvent(String roomId, String eventId, BingRule bingRule, long originServerTs) { - mRoomId = roomId; - mEventId = eventId; - mBingRule = bingRule; - mOriginServerTs = originServerTs; - } - } - - // max number of lines to display the notification text styles - private static final int MAX_NUMBER_NOTIFICATION_LINES = 10; - /** * Add a text style to a notification when there are several notified rooms. * - * @param context the context - * @param builder the notification builder - * @param notifiedEventsByRoomId the notified events by room ids + * @param context the context + * @param builder the notification builder + * @param roomsNotifications the rooms notifications */ private static void addTextStyleWithSeveralRooms(Context context, NotificationCompat.Builder builder, - NotifiedEvent eventToNotify, - boolean isInvitationEvent, - Map> notifiedEventsByRoomId) { - // TODO manage multi accounts - MXSession session = Matrix.getInstance(context).getDefaultSession(); - IMXStore store = session.getDataHandler().getStore(); + RoomsNotifications roomsNotifications) { NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - int sum = 0; - int roomsCount = 0; - - - List notificationsList = new ArrayList<>(); - - for (String roomId : notifiedEventsByRoomId.keySet()) { - Room room = session.getDataHandler().getRoom(roomId); - String roomName = getRoomName(context, session, room, null); - - List notifiedEvents = notifiedEventsByRoomId.get(roomId); - Event latestEvent = store.getEvent(notifiedEvents.get(notifiedEvents.size() - 1).mEventId, roomId); - - String text; - String header; - - EventDisplay eventDisplay = new RiotEventDisplay(context, latestEvent, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(false); - - if (room.isInvited()) { - header = roomName + ": "; - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - text = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; - } else if (1 == notifiedEvents.size()) { - eventDisplay = new RiotEventDisplay(context, latestEvent, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(false); - - header = roomName + ": " + room.getLiveState().getMemberName(latestEvent.getSender()) + " "; - - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - // the event might have been redacted - if (!TextUtils.isEmpty(textualDisplay)) { - text = textualDisplay.toString(); - } else { - text = ""; - } - } else { - header = roomName + ": "; - text = context.getString(R.string.notification_unread_notified_messages, notifiedEvents.size()); - } - - // ad the line if it makes sense - if (!TextUtils.isEmpty(text)) { - SpannableString notifiedLine = new SpannableString(header + text); - notifiedLine.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, header.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - notificationsList.add(new NotificationDisplay(latestEvent.getOriginServerTs(), notifiedLine)); - sum += notifiedEvents.size(); - roomsCount++; - } - } - - Collections.sort(notificationsList, mNotificationDisplaySort); - - if (notificationsList.size() > MAX_NUMBER_NOTIFICATION_LINES) { - notificationsList = notificationsList.subList(0, MAX_NUMBER_NOTIFICATION_LINES); - } - - for (NotificationDisplay notificationDisplay : notificationsList) { - inboxStyle.addLine(notificationDisplay.mMessage); + for (RoomNotifications roomNotifications : roomsNotifications.mRoomNotifications) { + SpannableString notifiedLine = new SpannableString(roomNotifications.mMessagesSummary); + notifiedLine.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, roomNotifications.mMessageHeader.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + inboxStyle.addLine(notifiedLine); } inboxStyle.setBigContentTitle(context.getString(R.string.riot_app_name)); - inboxStyle.setSummaryText(context.getString(R.string.notification_unread_notified_messages_in_room, sum, roomsCount)); + inboxStyle.setSummaryText(roomsNotifications.mSummaryText); builder.setStyle(inboxStyle); TaskStackBuilder stackBuilderTap = TaskStackBuilder.create(context); Intent roomIntentTap; - // sanity check - if ((null == eventToNotify) || TextUtils.isEmpty(eventToNotify.mRoomId)) { - // Build the pending intent for when the notification is clicked - roomIntentTap = new Intent(context, VectorHomeActivity.class); - } else { - // add the home page the activity stack - stackBuilderTap.addNextIntentWithParentStack(new Intent(context, VectorHomeActivity.class)); - if (isInvitationEvent) { - // for invitation the room preview must be displayed - roomIntentTap = CommonActivityUtils.buildIntentPreviewRoom(session.getMyUserId(), eventToNotify.mRoomId, context, VectorFakeRoomPreviewActivity.class); - } else { - roomIntentTap = new Intent(context, VectorRoomActivity.class); - roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, eventToNotify.mRoomId); - } + // add the home page the activity stack + stackBuilderTap.addNextIntentWithParentStack(new Intent(context, VectorHomeActivity.class)); + + if (roomsNotifications.mIsInvitationEvent) { + // for invitation the room preview must be displayed + roomIntentTap = CommonActivityUtils.buildIntentPreviewRoom(roomsNotifications.mSessionId, roomsNotifications.mRoomId, context, VectorFakeRoomPreviewActivity.class); + } else { + roomIntentTap = new Intent(context, VectorRoomActivity.class); + roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomsNotifications.mRoomId); } // the action must be unique else the parameters are ignored @@ -530,54 +369,6 @@ private static void addTextStyleWithSeveralRooms(Context context, context.getString(R.string.bottom_action_home), viewAllTask.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)); } - - // wearable - try { - long ts = 0; - Event latestEvent = null; - - // search the oldest message - for (String roomId : notifiedEventsByRoomId.keySet()) { - List notifiedEvents = notifiedEventsByRoomId.get(roomId); - Event event = store.getEvent(notifiedEvents.get(notifiedEvents.size() - 1).mEventId, roomId); - - if ((null != event) && (event.getOriginServerTs() > ts)) { - ts = event.getOriginServerTs(); - latestEvent = event; - } - } - - // if there is a valid latest message - if (null != latestEvent) { - Room room = store.getRoom(latestEvent.roomId); - - if (null != room) { - EventDisplay eventDisplay = new RiotEventDisplay(context, latestEvent, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(false); - String roomName = getRoomName(context, session, room, null); - - String message = roomName + ": " + room.getLiveState().getMemberName(latestEvent.getSender()) + " "; - - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - - // the event might have been redacted - if (!TextUtils.isEmpty(textualDisplay)) { - message += textualDisplay.toString(); - } - - NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); - NotificationCompat.Action action = - new NotificationCompat.Action.Builder(R.drawable.message_notification_transparent, - message, - stackBuilderTap.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) - .build(); - wearableExtender.addAction(action); - builder.extend(wearableExtender); - } - } - } catch (Exception e) { - Log.e(LOG_TAG, "## addTextStyleWithSeveralRooms() : WearableExtender failed " + e.getMessage()); - } } /** @@ -595,91 +386,53 @@ private static void addTextStyleWithSeveralRooms(Context context, * - "Room Name : XX unread messages" if there are many unread messages * - 'Room Name : Sender - Message body" if there is only one unread message. * - * @param context the context - * @param builder the notification builder - * @param eventToNotify the latest notified event - * @param isInvitationEvent true if the notified event is an invitation - * @param notifiedEventsByRoomId the notified events by room ids + * @param context the context + * @param builder the notification builder + * @param roomsNotifications the rooms notifications */ private static void addTextStyle(Context context, NotificationCompat.Builder builder, - NotifiedEvent eventToNotify, - boolean isInvitationEvent, - Map> notifiedEventsByRoomId) { + RoomsNotifications roomsNotifications) { // nothing to do - if (0 == notifiedEventsByRoomId.size()) { + if (0 == roomsNotifications.mRoomNotifications.size()) { return; } // when there are several rooms, the text style is not the same - if (notifiedEventsByRoomId.size() > 1) { - addTextStyleWithSeveralRooms(context, builder, eventToNotify, isInvitationEvent, notifiedEventsByRoomId); + if (roomsNotifications.mRoomNotifications.size() > 1) { + addTextStyleWithSeveralRooms(context, builder, roomsNotifications); return; } - // TODO manage multi accounts - MXSession session = Matrix.getInstance(context).getDefaultSession(); - IMXStore store = session.getDataHandler().getStore(); + SpannableString latestText = null; NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - String roomId = notifiedEventsByRoomId.keySet().iterator().next(); - - Room room = session.getDataHandler().getRoom(roomId); - String roomName = getRoomName(context, session, room, null); - - List notifiedEvents = notifiedEventsByRoomId.get(roomId); - int unreadCount = notifiedEvents.size(); - - // the messages are sorted from the oldest to the latest - Collections.reverse(notifiedEvents); - - if (notifiedEvents.size() > MAX_NUMBER_NOTIFICATION_LINES) { - notifiedEvents = notifiedEvents.subList(0, MAX_NUMBER_NOTIFICATION_LINES); + for (CharSequence sequence : roomsNotifications.mReversedMessagesList) { + inboxStyle.addLine(latestText = new SpannableString(sequence)); } - SpannableString latestText = null; - - for (NotifiedEvent notifiedEvent : notifiedEvents) { - Event event = store.getEvent(notifiedEvent.mEventId, notifiedEvent.mRoomId); - EventDisplay eventDisplay = new RiotEventDisplay(context, event, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(true); - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - - if (!TextUtils.isEmpty(textualDisplay)) { - inboxStyle.addLine(latestText = new SpannableString(textualDisplay)); - } - } - inboxStyle.setBigContentTitle(roomName); + inboxStyle.setBigContentTitle(roomsNotifications.mContentTitle); // adapt the notification display to the number of notified messages - if ((1 == notifiedEvents.size()) && (null != latestText)) { + if ((1 == roomsNotifications.mReversedMessagesList.size()) && (null != latestText)) { builder.setStyle(new NotificationCompat.BigTextStyle().bigText(latestText)); } else { - if (unreadCount > MAX_NUMBER_NOTIFICATION_LINES) { - inboxStyle.setSummaryText(context.getString(R.string.notification_unread_notified_messages, unreadCount)); + if (!TextUtils.isEmpty(roomsNotifications.mSummaryText)) { + inboxStyle.setSummaryText(roomsNotifications.mSummaryText); } - builder.setStyle(inboxStyle); } // do not offer to quick respond if the user did not dismiss the previous one if (!LockScreenActivity.isDisplayingALockScreenActivity()) { - if (!isInvitationEvent) { - Event event = store.getEvent(eventToNotify.mEventId, eventToNotify.mRoomId); - RoomMember member = room.getMember(event.getSender()); + if (!roomsNotifications.mIsInvitationEvent) { // offer to type a quick answer (i.e. without launching the application) Intent quickReplyIntent = new Intent(context, LockScreenActivity.class); - quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId); - quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, (null == member) ? event.getSender() : member.getName()); - - EventDisplay eventDisplay = new RiotEventDisplay(context, event, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(false); - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - String body = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; - - quickReplyIntent.putExtra(LockScreenActivity.EXTRA_MESSAGE_BODY, body); + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomsNotifications.mRoomId); + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, roomsNotifications.mSenderName); + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_MESSAGE_BODY, roomsNotifications.mQuickReplyBody); // the action must be unique else the parameters are ignored quickReplyIntent.setAction(QUICK_LAUNCH_ACTION + ((int) (System.currentTimeMillis()))); @@ -692,8 +445,8 @@ private static void addTextStyle(Context context, { // offer to type a quick reject button Intent leaveIntent = new Intent(context, JoinScreenActivity.class); - leaveIntent.putExtra(JoinScreenActivity.EXTRA_ROOM_ID, roomId); - leaveIntent.putExtra(JoinScreenActivity.EXTRA_MATRIX_ID, session.getMyUserId()); + leaveIntent.putExtra(JoinScreenActivity.EXTRA_ROOM_ID, roomsNotifications.mRoomId); + leaveIntent.putExtra(JoinScreenActivity.EXTRA_MATRIX_ID, roomsNotifications.mSessionId); leaveIntent.putExtra(JoinScreenActivity.EXTRA_REJECT, true); // the action must be unique else the parameters are ignored @@ -708,8 +461,8 @@ private static void addTextStyle(Context context, { // offer to type a quick accept button Intent acceptIntent = new Intent(context, JoinScreenActivity.class); - acceptIntent.putExtra(JoinScreenActivity.EXTRA_ROOM_ID, roomId); - acceptIntent.putExtra(JoinScreenActivity.EXTRA_MATRIX_ID, session.getMyUserId()); + acceptIntent.putExtra(JoinScreenActivity.EXTRA_ROOM_ID, roomsNotifications.mRoomId); + acceptIntent.putExtra(JoinScreenActivity.EXTRA_MATRIX_ID, roomsNotifications.mSessionId); acceptIntent.putExtra(JoinScreenActivity.EXTRA_JOIN, true); // the action must be unique else the parameters are ignored @@ -725,12 +478,12 @@ private static void addTextStyle(Context context, // Build the pending intent for when the notification is clicked Intent roomIntentTap; - if (isInvitationEvent) { + if (roomsNotifications.mIsInvitationEvent) { // for invitation the room preview must be displayed - roomIntentTap = CommonActivityUtils.buildIntentPreviewRoom(session.getMyUserId(), roomId, context, VectorFakeRoomPreviewActivity.class); + roomIntentTap = CommonActivityUtils.buildIntentPreviewRoom(roomsNotifications.mSessionId, roomsNotifications.mRoomId, context, VectorFakeRoomPreviewActivity.class); } else { roomIntentTap = new Intent(context, VectorRoomActivity.class); - roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId); + roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomsNotifications.mRoomId); } // the action must be unique else the parameters are ignored roomIntentTap.setAction(TAP_TO_VIEW_ACTION + ((int) (System.currentTimeMillis()))); @@ -748,33 +501,16 @@ private static void addTextStyle(Context context, stackBuilderTap.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)); // wearable - if (!isInvitationEvent) { + if (!roomsNotifications.mIsInvitationEvent) { try { - Event latestEvent = store.getEvent(notifiedEvents.get(notifiedEvents.size() - 1).mEventId, roomId); - - // if there is a valid latest message - if (null != latestEvent) { - EventDisplay eventDisplay = new RiotEventDisplay(context, latestEvent, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(false); - - String message = roomName + ": " + room.getLiveState().getMemberName(latestEvent.getSender()) + " "; - - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - - // the event might have been redacted - if (!TextUtils.isEmpty(textualDisplay)) { - message += textualDisplay.toString(); - } - - NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); - NotificationCompat.Action action = - new NotificationCompat.Action.Builder(R.drawable.message_notification_transparent, - message, - stackBuilderTap.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) - .build(); - wearableExtender.addAction(action); - builder.extend(wearableExtender); - } + NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); + NotificationCompat.Action action = + new NotificationCompat.Action.Builder(R.drawable.message_notification_transparent, + roomsNotifications.mWearableMessage, + stackBuilderTap.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + wearableExtender.addAction(action); + builder.extend(wearableExtender); } catch (Exception e) { Log.e(LOG_TAG, "## addTextStyleWithSeveralRooms() : WearableExtender failed " + e.getMessage()); } @@ -827,6 +563,30 @@ private static void manageNotificationSound(Context context, NotificationCompat. } } + /** + * Build a notification from the cached RoomsNotifications instance. + * + * @param context the context + * @param isBackground true if it is background notification + * @return the notification + */ + public static Notification buildMessageNotification(Context context, boolean isBackground) { + + Notification notification = null; + try { + RoomsNotifications roomsNotifications = RoomsNotifications.loadRoomsNotifications(context); + + if (null != roomsNotifications) { + notification = buildMessageNotification(context, roomsNotifications, new BingRule(), isBackground); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## buildMessageNotification() : failed " + e.getMessage()); + } + + return notification; + } + + /** * Build a notification * @@ -840,81 +600,60 @@ public static Notification buildMessageNotification(Context context, Map> notifiedEventsByRoomId, NotifiedEvent eventToNotify, boolean isBackground) { - try { - // TODO manage multi accounts - MXSession session = Matrix.getInstance(context).getDefaultSession(); - IMXStore store = session.getDataHandler().getStore(); - if (null == store) { - Log.e(LOG_TAG, "## buildMessageNotification() : null store"); - return null; - } - - Room room = store.getRoom(eventToNotify.mRoomId); - Event event = store.getEvent(eventToNotify.mEventId, eventToNotify.mRoomId); - - // sanity check - if ((null == room) || (null == event)) { - if (null == room) { - Log.e(LOG_TAG, "## buildMessageNotification() : null room " + eventToNotify.mRoomId); - } else { - Log.e(LOG_TAG, "## buildMessageNotification() : null event " + eventToNotify.mEventId + " " + eventToNotify.mRoomId); - } - return null; - } - - BingRule bingRule = eventToNotify.mBingRule; - - boolean isInvitationEvent = false; + Notification notification = null; + try { + RoomsNotifications roomsNotifications = new RoomsNotifications(eventToNotify, notifiedEventsByRoomId); + notification = buildMessageNotification(context, roomsNotifications, eventToNotify.mBingRule, isBackground); + // cache the value + RoomsNotifications.saveRoomNotifications(context, roomsNotifications); + } catch (Exception e) { + Log.e(LOG_TAG, "## buildMessageNotification() : failed " + e.getMessage()); + } - EventDisplay eventDisplay = new RiotEventDisplay(context, event, room.getLiveState()); - eventDisplay.setPrependMessagesWithAuthor(true); - CharSequence textualDisplay = eventDisplay.getTextualDisplay(); - String body = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; + return notification; + } - if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType())) { - try { - isInvitationEvent = "invite".equals(event.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString()); - } catch (Exception e) { - Log.e(LOG_TAG, "prepareNotification : invitation parsing failed"); - } - } + /** + * Build a notification + * + * @param context the context + * @param roomsNotifications the rooms notifications + * @param bingRule the bing rule + * @param isBackground true if it is background notification + * @return the notification + */ + private static Notification buildMessageNotification(Context context, + RoomsNotifications roomsNotifications, + BingRule bingRule, + boolean isBackground) { + try { Bitmap largeBitmap = null; // when the event is an invitation one // don't check if the sender ID is known because the members list are not yet downloaded - if (!isInvitationEvent) { + if (!roomsNotifications.mIsInvitationEvent) { // is there any avatar url - if (!TextUtils.isEmpty(room.getAvatarUrl())) { - int size = context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size); - - // check if the thumbnail is already downloaded - File f = session.getMediasCache().thumbnailCacheFile(room.getAvatarUrl(), size); - - if (null != f) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - try { - largeBitmap = BitmapFactory.decodeFile(f.getPath(), options); - } catch (OutOfMemoryError oom) { - Log.e(LOG_TAG, "decodeFile failed with an oom"); - } - } else { - session.getMediasCache().loadAvatarThumbnail(session.getHomeServerConfig(), new ImageView(context), room.getAvatarUrl(), size); + if (!TextUtils.isEmpty(roomsNotifications.mRoomAvatarPath)) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + try { + largeBitmap = BitmapFactory.decodeFile(roomsNotifications.mRoomAvatarPath, options); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "decodeFile failed with an oom"); } } } Log.d(LOG_TAG, "prepareNotification : with sound " + bingRule.isDefaultNotificationSound(bingRule.getNotificationSound())); - String roomName = getRoomName(context, session, room, event); - addNotificationChannels(context); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID); - builder.setWhen(event.getOriginServerTs()); - builder.setContentTitle(roomName); - builder.setContentText(body); + builder.setWhen(roomsNotifications.mContentTs); + builder.setContentTitle(roomsNotifications.mContentTitle); + builder.setContentText(roomsNotifications.mContentText); builder.setGroup(context.getString(R.string.riot_app_name)); builder.setGroupSummary(true); @@ -922,14 +661,14 @@ public static Notification buildMessageNotification(Context context, builder.setDeleteIntent(PendingIntent.getBroadcast(context.getApplicationContext(), 0, new Intent(context.getApplicationContext(), DismissNotificationReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT)); try { - addTextStyle(context, builder, eventToNotify, isInvitationEvent, notifiedEventsByRoomId); + addTextStyle(context, builder, roomsNotifications); } catch (Exception e) { Log.e(LOG_TAG, "## buildMessageNotification() : addTextStyle failed " + e.getMessage()); } // only one room : display the large bitmap (it should be the room avatar // several rooms : display the Riot avatar - if (notifiedEventsByRoomId.keySet().size() == 1) { + if (roomsNotifications.mRoomNotifications.size() == 1) { if (null != largeBitmap) { largeBitmap = NotificationUtils.createSquareBitmap(largeBitmap); builder.setLargeIcon(largeBitmap); @@ -967,7 +706,7 @@ public static Notification buildMessagesListNotification(Context context, List mRoomNotificationsComparator = new Comparator() { + @Override + public int compare(RoomNotifications lhs, RoomNotifications rhs) { + long t0 = lhs.mLatestEventTs; + long t1 = rhs.mLatestEventTs; + + if (t0 > t1) { + return -1; + } else if (t0 < t1) { + return +1; + } + return 0; + } + }; + + // empty constructor + public RoomNotifications() { + } + + /* + * ********************************************************************************************* + * Parcelable + * ********************************************************************************************* + */ + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mRoomId); + out.writeString(mRoomName); + out.writeString(mMessageHeader); + TextUtils.writeToParcel(mMessagesSummary, out, 0); + out.writeLong(mLatestEventTs); + + out.writeString(mSenderName); + out.writeInt(mUnreadMessagesCount); + } + + /** + * Creator from a parcel + * + * @param in the parcel + */ + private RoomNotifications(Parcel in) { + mRoomId = in.readString(); + mRoomName = in.readString(); + mMessageHeader = in.readString(); + mMessagesSummary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mLatestEventTs = in.readLong(); + mSenderName = in.readString(); + mUnreadMessagesCount = in.readInt(); + } + + public final static Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + public RoomNotifications createFromParcel(Parcel p) { + return new RoomNotifications(p); + } + + public RoomNotifications[] newArray(int size) { + return new RoomNotifications[size]; + } + }; +} diff --git a/vector/src/main/java/im/vector/notifications/RoomsNotifications.java b/vector/src/main/java/im/vector/notifications/RoomsNotifications.java new file mode 100755 index 0000000000..d4876f8841 --- /dev/null +++ b/vector/src/main/java/im/vector/notifications/RoomsNotifications.java @@ -0,0 +1,613 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.notifications; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.widget.ImageView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.data.Room; +import org.matrix.androidsdk.data.store.IMXStore; +import org.matrix.androidsdk.rest.model.Event; +import org.matrix.androidsdk.rest.model.RoomMember; +import org.matrix.androidsdk.rest.model.User; +import org.matrix.androidsdk.util.EventDisplay; +import org.matrix.androidsdk.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import im.vector.Matrix; +import im.vector.R; +import im.vector.VectorApp; +import im.vector.activity.LockScreenActivity; +import im.vector.util.RiotEventDisplay; +import im.vector.util.VectorUtils; + +/** + * RoomsNotifications + */ +public class RoomsNotifications implements Parcelable { + private static final String LOG_TAG = RoomsNotifications.class.getSimpleName(); + + // max number of lines to display the notification text styles + static final int MAX_NUMBER_NOTIFICATION_LINES = 10; + + /****** Parcelable items ********/ + // the session id + String mSessionId = ""; + + // the notified event room Id + String mRoomId = ""; + + // the notification summary + String mSummaryText = ""; + + // latest message with sender header + String mQuickReplyBody = ""; + + // wearable notification message + String mWearableMessage = ""; + + // true when the notified event is an invitation one + boolean mIsInvitationEvent = false; + + // the room avatar + String mRoomAvatarPath = ""; + + // notified message TS + long mContentTs = -1; + + // content title + String mContentTitle = ""; + + // the context text + String mContentText = ""; + + String mSenderName = ""; + + // the notifications list + List mRoomNotifications = new ArrayList<>(); + + // messages list + List mReversedMessagesList = new ArrayList<>(); + + /****** others items ********/ + // notified event + private NotifiedEvent mEventToNotify; + + // notified events by room id + private Map> mNotifiedEventsByRoomId; + + // notification details + private Context mContext; + private MXSession mSession; + private Room mRoom; + private Event mEvent; + + /** + * Empty constructor + */ + public RoomsNotifications() { + } + + /** + * Constructor + * + * @param anEventToNotify the event to notify + * @param someNotifiedEventsByRoomId the notified events + */ + public RoomsNotifications(NotifiedEvent anEventToNotify, + Map> someNotifiedEventsByRoomId) { + mContext = VectorApp.getInstance(); + mSession = Matrix.getInstance(mContext).getDefaultSession(); + IMXStore store = mSession.getDataHandler().getStore(); + + mEventToNotify = anEventToNotify; + mNotifiedEventsByRoomId = someNotifiedEventsByRoomId; + + // the session id + mSessionId = mSession.getMyUserId(); + mRoomId = anEventToNotify.mRoomId; + mRoom = store.getRoom(mEventToNotify.mRoomId); + mEvent = store.getEvent(mEventToNotify.mEventId, mEventToNotify.mRoomId); + + // sanity check + if ((null == mRoom) || (null == mEvent)) { + if (null == mRoom) { + Log.e(LOG_TAG, "## RoomsNotifications() : null room " + mEventToNotify.mRoomId); + } else { + Log.e(LOG_TAG, "## RoomsNotifications() : null event " + mEventToNotify.mEventId + " " + mEventToNotify.mRoomId); + } + return; + } + + mIsInvitationEvent = false; + + EventDisplay eventDisplay = new RiotEventDisplay(mContext, mEvent, mRoom.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(true); + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + String body = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; + + if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(mEvent.getType())) { + try { + mIsInvitationEvent = "invite".equals(mEvent.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString()); + } catch (Exception e) { + Log.e(LOG_TAG, "RoomsNotifications : invitation parsing failed"); + } + } + // when the event is an invitation one + // don't check if the sender ID is known because the members list are not yet downloaded + if (!mIsInvitationEvent) { + int size = mContext.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size); + + File f = mSession.getMediasCache().thumbnailCacheFile(mRoom.getAvatarUrl(), size); + + if (null != f) { + mRoomAvatarPath = f.getPath(); + } else { + // prepare for the next time + mSession.getMediasCache().loadAvatarThumbnail(mSession.getHomeServerConfig(), new ImageView(mContext), mRoom.getAvatarUrl(), size); + } + } + + String roomName = getRoomName(mContext, mSession, mRoom, mEvent); + + mContentTs = mEvent.getOriginServerTs(); + mContentTitle = roomName; + mContentText = body; + + RoomMember member = mRoom.getMember(mEvent.getSender()); + mSenderName = (null == member) ? mEvent.getSender() : member.getName(); + + boolean singleRoom = (mNotifiedEventsByRoomId.size() == 1); + + if (singleRoom) { + initSingleRoom(); + } else { + initMultiRooms(); + } + } + + /** + * Init for a single room notifications + */ + private void initSingleRoom() { + RoomNotifications roomNotifications = new RoomNotifications(); + mRoomNotifications.add(roomNotifications); + roomNotifications.mRoomId = mEvent.roomId; + roomNotifications.mRoomName = mContentTitle; + + List notifiedEvents = mNotifiedEventsByRoomId.get(roomNotifications.mRoomId); + int unreadCount = notifiedEvents.size(); + + // the messages are sorted from the oldest to the latest + Collections.reverse(notifiedEvents); + + if (notifiedEvents.size() > MAX_NUMBER_NOTIFICATION_LINES) { + notifiedEvents = notifiedEvents.subList(0, MAX_NUMBER_NOTIFICATION_LINES); + } + + SpannableString latestText = null; + IMXStore store = mSession.getDataHandler().getStore(); + + for (NotifiedEvent notifiedEvent : notifiedEvents) { + Event event = store.getEvent(notifiedEvent.mEventId, notifiedEvent.mRoomId); + EventDisplay eventDisplay = new RiotEventDisplay(mContext, event, mRoom.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(true); + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + + if (!TextUtils.isEmpty(textualDisplay)) { + mReversedMessagesList.add(textualDisplay); + } + } + + // adapt the notification display to the number of notified messages + if ((1 == notifiedEvents.size()) && (null != latestText)) { + roomNotifications.mMessagesSummary = latestText; + } else { + if (unreadCount > MAX_NUMBER_NOTIFICATION_LINES) { + mSummaryText = mContext.getString(R.string.notification_unread_notified_messages, unreadCount); + } + } + + // do not offer to quick respond if the user did not dismiss the previous one + if (!LockScreenActivity.isDisplayingALockScreenActivity()) { + if (!mIsInvitationEvent) { + Event event = store.getEvent(mEventToNotify.mEventId, mEventToNotify.mRoomId); + RoomMember member = mRoom.getMember(event.getSender()); + roomNotifications.mSenderName = (null == member) ? event.getSender() : member.getName(); + + EventDisplay eventDisplay = new RiotEventDisplay(mContext, event, mRoom.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(false); + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + mQuickReplyBody = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; + } + } + + initWearableMessage(mContext, mRoom, store.getEvent(notifiedEvents.get(notifiedEvents.size() - 1).mEventId, roomNotifications.mRoomId), mIsInvitationEvent); + } + + /** + * Init for multi rooms notifications + */ + private void initMultiRooms() { + IMXStore store = mSession.getDataHandler().getStore(); + + int sum = 0; + int roomsCount = 0; + + for (String roomId : mNotifiedEventsByRoomId.keySet()) { + Room room = mSession.getDataHandler().getRoom(roomId); + String roomName = getRoomName(mContext, mSession, room, null); + + List notifiedEvents = mNotifiedEventsByRoomId.get(roomId); + Event latestEvent = store.getEvent(notifiedEvents.get(notifiedEvents.size() - 1).mEventId, roomId); + + String text; + String header; + + EventDisplay eventDisplay = new RiotEventDisplay(mContext, latestEvent, room.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(false); + + if (room.isInvited()) { + header = roomName + ": "; + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + text = !TextUtils.isEmpty(textualDisplay) ? textualDisplay.toString() : ""; + } else if (1 == notifiedEvents.size()) { + eventDisplay = new RiotEventDisplay(mContext, latestEvent, room.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(false); + + header = roomName + ": " + room.getLiveState().getMemberName(latestEvent.getSender()) + " "; + + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + + // the event might have been redacted + if (!TextUtils.isEmpty(textualDisplay)) { + text = textualDisplay.toString(); + } else { + text = ""; + } + } else { + header = roomName + ": "; + text = mContext.getString(R.string.notification_unread_notified_messages, notifiedEvents.size()); + } + + // ad the line if it makes sense + if (!TextUtils.isEmpty(text)) { + RoomNotifications roomNotifications = new RoomNotifications(); + mRoomNotifications.add(roomNotifications); + + roomNotifications.mRoomId = roomId; + roomNotifications.mLatestEventTs = latestEvent.getOriginServerTs(); + roomNotifications.mMessageHeader = header; + roomNotifications.mMessagesSummary = header + text; + sum += notifiedEvents.size(); + roomsCount++; + } + } + + Collections.sort(mRoomNotifications, RoomNotifications.mRoomNotificationsComparator); + + if (mRoomNotifications.size() > MAX_NUMBER_NOTIFICATION_LINES) { + mRoomNotifications = mRoomNotifications.subList(0, MAX_NUMBER_NOTIFICATION_LINES); + } + + mSummaryText = mContext.getString(R.string.notification_unread_notified_messages_in_room, sum, roomsCount); + } + + /** + * Compute the wearable message + * + * @param context the context + * @param room the room + * @param latestEvent the latest event + * @param isInvitationEvent true if it is an invitaion + */ + private void initWearableMessage(Context context, Room room, Event latestEvent, boolean isInvitationEvent) { + if (!isInvitationEvent) { + // if there is a valid latest message + if ((null != latestEvent) && (null != room)) { + MXSession session = Matrix.getInstance(context).getDefaultSession(); + String roomName = getRoomName(context, session, room, null); + + EventDisplay eventDisplay = new RiotEventDisplay(context, latestEvent, room.getLiveState()); + eventDisplay.setPrependMessagesWithAuthor(false); + + mWearableMessage = roomName + ": " + room.getLiveState().getMemberName(latestEvent.getSender()) + " "; + CharSequence textualDisplay = eventDisplay.getTextualDisplay(); + + // the event might have been redacted + if (!TextUtils.isEmpty(textualDisplay)) { + mWearableMessage += textualDisplay.toString(); + } + } + } + } + + /* + * ********************************************************************************************* + * Parcelable + * ********************************************************************************************* + */ + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mSessionId); + out.writeString(mRoomId); + out.writeString(mSummaryText); + out.writeString(mQuickReplyBody); + out.writeString(mWearableMessage); + out.writeInt(mIsInvitationEvent ? 1 : 0); + out.writeString(mRoomAvatarPath); + out.writeLong(mContentTs); + + out.writeString(mContentTitle); + out.writeString(mContentText); + out.writeString(mSenderName); + + RoomNotifications[] roomNotifications = new RoomNotifications[mRoomNotifications.size()]; + mRoomNotifications.toArray(roomNotifications); + out.writeArray(roomNotifications); + + out.writeInt(mReversedMessagesList.size()); + for (CharSequence sequence : mReversedMessagesList) { + TextUtils.writeToParcel(sequence, out, 0); + } + } + + /** + * Constructor from the parcel. + * + * @param in the parcel + */ + private void init(Parcel in) { + mSessionId = in.readString(); + mRoomId = in.readString(); + mSummaryText = in.readString(); + mQuickReplyBody = in.readString(); + mWearableMessage = in.readString(); + mIsInvitationEvent = (1 == in.readInt()) ? true : false; + mRoomAvatarPath = in.readString(); + mContentTs = in.readLong(); + + mContentTitle = in.readString(); + mContentText = in.readString(); + mSenderName = in.readString(); + + Object[] roomNotificationsAasVoid = in.readArray(RoomNotifications.class.getClassLoader()); + for (Object object : roomNotificationsAasVoid) { + mRoomNotifications.add((RoomNotifications) object); + } + + int count = in.readInt(); + mReversedMessagesList = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + mReversedMessagesList.add(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); + } + } + + /** + * Parcelable creator + */ + public final static Parcelable.Creator CREATOR = new Parcelable.Creator() { + public RoomsNotifications createFromParcel(Parcel p) { + RoomsNotifications res = new RoomsNotifications(); + res.init(p); + return res; + } + + public RoomsNotifications[] newArray(int size) { + return new RoomsNotifications[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /* + * ********************************************************************************************* + * Serialisation + * ********************************************************************************************* + */ + + private static final String ROOMS_NOTIFICATIONS_FILE_NAME = "ROOMS_NOTIFICATIONS_FILE_NAME"; + + + /** + * @return byte[] from the class + */ + private byte[] marshall() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + return bytes; + } + + /** + * Create a RoomsNotifications instance from a bytes[]. + * + * @param bytes the bytes array + */ + private RoomsNotifications(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + + init(parcel); + parcel.recycle(); + } + + /** + * Delete the cached RoomNotifications + * + * @param context the context + */ + public static void deleteCachedRoomNotifications(Context context) { + File file = new File(context.getApplicationContext().getCacheDir(), ROOMS_NOTIFICATIONS_FILE_NAME); + + if (file.exists()) { + file.delete(); + } + } + + /** + * Save the roomsNotifications instance into the file system. + * + * @param context the context + * @param roomsNotifications the roomsNotifications instance + */ + public static void saveRoomNotifications(Context context, RoomsNotifications roomsNotifications) { + deleteCachedRoomNotifications(context); + + // no notified messages + if (roomsNotifications.mRoomNotifications.isEmpty()) { + return; + } + + ByteArrayInputStream fis = null; + FileOutputStream fos = null; + + try { + fis = new ByteArrayInputStream(roomsNotifications.marshall()); + fos = new FileOutputStream(new File(context.getApplicationContext().getCacheDir(), ROOMS_NOTIFICATIONS_FILE_NAME)); + + byte[] readData = new byte[1024]; + int len; + + while ((len = fis.read(readData, 0, 1024)) > 0) { + fos.write(readData, 0, len); + } + } catch (Throwable t) { + Log.e(LOG_TAG, "## saveRoomNotifications() failed " + t.getMessage()); + } + + try { + if (null != fis) { + fis.close(); + } + + if (null != fos) { + fos.close(); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## saveRoomNotifications() failed " + e.getMessage()); + } + } + + /** + * Load a saved RoomsNotifications from the file system + * + * @param context the context + * @return a RoomsNotifications instance if found + */ + public static RoomsNotifications loadRoomsNotifications(Context context) { + File file = new File(context.getApplicationContext().getCacheDir(), ROOMS_NOTIFICATIONS_FILE_NAME); + + // test if the file exits + if (!file.exists()) { + return null; + } + + RoomsNotifications roomsNotifications = null; + FileInputStream fis = null; + ByteArrayOutputStream fos = null; + + try { + fis = new FileInputStream(file); + fos = new ByteArrayOutputStream(); + + + byte[] readData = new byte[1024]; + int len; + + while ((len = fis.read(readData, 0, 1024)) > 0) { + fos.write(readData, 0, len); + } + + roomsNotifications = new RoomsNotifications(fos.toByteArray()); + } catch (Throwable t) { + Log.e(LOG_TAG, "## loadRoomsNotifications() failed " + t.getMessage()); + } + + try { + if (null != fis) { + fis.close(); + } + + if (null != fos) { + fos.close(); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## loadRoomsNotifications() failed " + e.getMessage()); + } + + return roomsNotifications; + } + + /* + * ********************************************************************************************* + * Utils + * ********************************************************************************************* + */ + + /** + * Retrieve the room name. + * + * @param session the session + * @param room the room + * @param event the event + * @return the room name + */ + public static String getRoomName(Context context, MXSession session, Room room, Event event) { + String roomName = VectorUtils.getRoomDisplayName(context, session, room); + + // avoid displaying the room Id + // try to find the sender display name + if (TextUtils.equals(roomName, room.getRoomId())) { + roomName = room.getName(session.getMyUserId()); + + // avoid room Id as name + if (TextUtils.equals(roomName, room.getRoomId()) && (null != event)) { + User user = session.getDataHandler().getStore().getUser(event.sender); + + if (null != user) { + roomName = user.displayname; + } else { + roomName = event.sender; + } + } + } + + return roomName; + } +} diff --git a/vector/src/main/java/im/vector/preference/UserAvatarPreference.java b/vector/src/main/java/im/vector/preference/UserAvatarPreference.java index dd62d5b63e..8ee4e50ca0 100755 --- a/vector/src/main/java/im/vector/preference/UserAvatarPreference.java +++ b/vector/src/main/java/im/vector/preference/UserAvatarPreference.java @@ -17,8 +17,8 @@ package im.vector.preference; import android.content.Context; +import android.os.Bundle; import android.preference.EditTextPreference; -import android.preference.PreferenceScreen; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -74,4 +74,9 @@ public void setSession(MXSession session) { mSession = session; refreshAvatar(); } + + @Override + protected void showDialog(Bundle state) { + // do nothing + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/preference/VectorGroupPreference.java b/vector/src/main/java/im/vector/preference/VectorGroupPreference.java new file mode 100644 index 0000000000..810956e0bd --- /dev/null +++ b/vector/src/main/java/im/vector/preference/VectorGroupPreference.java @@ -0,0 +1,151 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.preference; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.data.MyUser; +import org.matrix.androidsdk.rest.model.group.Group; + +import im.vector.R; +import im.vector.util.VectorUtils; + +public class VectorGroupPreference extends VectorSwitchPreference { + private static final String LOG_TAG = VectorGroupPreference.class.getSimpleName(); + + private Context mContext; + private ImageView mAvatarView; + + private Group mGroup; + private MXSession mSession; + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default + * @param defStyle Theme attribute defining the default style options + */ + public VectorGroupPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default + */ + public VectorGroupPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public VectorGroupPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + /** + * Construct a new SwitchPreference with default style options. + * + * @param context The Context that will style this preference + */ + public VectorGroupPreference(Context context) { + super(context, null); + init(context); + } + + @Override + protected View onCreateView(ViewGroup parent) { + View createdView = super.onCreateView(parent); + + try { + // insert the group avatar to the left + final ImageView iconView = createdView.findViewById(android.R.id.icon); + + ViewParent iconViewParent = iconView.getParent(); + + while (null != iconViewParent.getParent()) { + iconViewParent = iconViewParent.getParent(); + } + + LayoutInflater inflater = LayoutInflater.from(mContext); + FrameLayout layout = (FrameLayout) inflater.inflate(R.layout.vector_settings_round_group_avatar, null, false); + mAvatarView = layout.findViewById(R.id.avatar_img); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.CENTER; + layout.setLayoutParams(params); + ((LinearLayout)iconViewParent).addView(layout, 0); + + refreshAvatar(); + } catch (Exception e) { + mAvatarView = null; + } + + return createdView; + } + + /** + * Init the group information + * + * @param group the group + * @param session the session + */ + public void setGroup(Group group, MXSession session) { + mGroup = group; + mSession = session; + + refreshAvatar(); + } + + /** + * Refresh the avatar + */ + public void refreshAvatar() { + if ((null != mAvatarView) && (null != mSession) && (null != mGroup)) { + VectorUtils.loadGroupAvatar(mContext, mSession, mAvatarView ,mGroup); + } + } + + /** + * Common init method. + * + * @param context the context + */ + private void init(Context context) { + // Force the use of SwitchCompat component + setWidgetLayoutResource(R.layout.preference_switch_layout); + mContext = context; + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/receiver/VectorUniversalLinkReceiver.java b/vector/src/main/java/im/vector/receiver/VectorUniversalLinkReceiver.java index 3ac9972fa6..2ebb32c112 100644 --- a/vector/src/main/java/im/vector/receiver/VectorUniversalLinkReceiver.java +++ b/vector/src/main/java/im/vector/receiver/VectorUniversalLinkReceiver.java @@ -46,6 +46,7 @@ import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; import im.vector.activity.LoginActivity; +import im.vector.activity.VectorGroupDetailsActivity; import im.vector.activity.VectorHomeActivity; import im.vector.activity.VectorMemberDetailsActivity; import im.vector.activity.VectorRoomActivity; @@ -81,6 +82,7 @@ public class VectorUniversalLinkReceiver extends BroadcastReceiver { // index of each item in path public static final String ULINK_ROOM_ID_OR_ALIAS_KEY = "ULINK_ROOM_ID_OR_ALIAS_KEY"; public static final String ULINK_MATRIX_USER_ID_KEY = "ULINK_MATRIX_USER_ID_KEY"; + public static final String ULINK_GROUP_ID_KEY = "ULINK_GROUP_ID_KEY"; private static final String ULINK_EVENT_ID_KEY = "ULINK_EVENT_ID_KEY"; /*public static final String ULINK_EMAIL_ID_KEY = "email"; public static final String ULINK_SIGN_URL_KEY = "signurl"; @@ -181,6 +183,8 @@ public void onReceive(final Context aContext, final Intent aIntent) { manageRoomOnActivity(aContext); } else if (mParameters.containsKey(ULINK_MATRIX_USER_ID_KEY)) { manageMemberDetailsActivity(aContext); + } else if (mParameters.containsKey(ULINK_GROUP_ID_KEY)) { + manageGroupDetailsActivity(aContext); } else { Log.e(LOG_TAG, "## onReceive() : nothing to do"); } @@ -255,6 +259,32 @@ public void run() { } } + /** + * Start the universal link management when the login process is done. + * If there is no active activity, launch the home activity + * + * @param aContext the context. + */ + private void manageGroupDetailsActivity(final Context aContext) { + Log.d(LOG_TAG, "## manageMemberDetailsActivity() : open the group" + mParameters.get(ULINK_GROUP_ID_KEY)); + + final Activity currentActivity = VectorApp.getCurrentActivity(); + + if (null != currentActivity) { + Intent startRoomInfoIntent = new Intent(currentActivity, VectorGroupDetailsActivity.class); + startRoomInfoIntent.putExtra(VectorGroupDetailsActivity.EXTRA_GROUP_ID, mParameters.get(ULINK_GROUP_ID_KEY)); + startRoomInfoIntent.putExtra(VectorGroupDetailsActivity.EXTRA_MATRIX_ID, mSession.getCredentials().userId); + currentActivity.startActivity(startRoomInfoIntent); + } else { + // clear the activity stack to home activity + Intent intent = new Intent(aContext, VectorHomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(VectorHomeActivity.EXTRA_GROUP_ID, mParameters.get(ULINK_GROUP_ID_KEY)); + aContext.startActivity(intent); + } + } + + /** * Manage the room presence. * Check the URL room ID format: if room ID is provided as an alias, we translate it @@ -430,6 +460,8 @@ public static HashMap parseUniversalLink(Uri uri) { map.put(ULINK_MATRIX_USER_ID_KEY, firstParam); } else if (MXSession.isRoomAlias(firstParam) || MXSession.isRoomId(firstParam)) { map.put(ULINK_ROOM_ID_OR_ALIAS_KEY, firstParam); + } else if (MXSession.isGroupId(firstParam)) { + map.put(ULINK_GROUP_ID_KEY, firstParam); } // room id only ? diff --git a/vector/src/main/java/im/vector/services/EventStreamService.java b/vector/src/main/java/im/vector/services/EventStreamService.java index cc7862397a..edc37f231e 100755 --- a/vector/src/main/java/im/vector/services/EventStreamService.java +++ b/vector/src/main/java/im/vector/services/EventStreamService.java @@ -70,9 +70,12 @@ import im.vector.ViewedRoomTracker; import im.vector.activity.VectorHomeActivity; import im.vector.gcm.GcmRegistrationManager; +import im.vector.notifications.NotificationUtils; +import im.vector.notifications.NotifiedEvent; +import im.vector.notifications.RoomsNotifications; import im.vector.receiver.DismissNotificationReceiver; import im.vector.util.CallsManager; -import im.vector.util.NotificationUtils; +import im.vector.util.PreferencesManager; import im.vector.util.RiotEventDisplay; /** @@ -105,19 +108,30 @@ public enum StreamAction { */ public static final String EXTRA_STREAM_ACTION = "EventStreamService.EXTRA_STREAM_ACTION"; public static final String EXTRA_MATRIX_IDS = "EventStreamService.EXTRA_MATRIX_IDS"; - private static final String EXTRA_AUTO_RESTART_ACTION = "EventStreamService.EXTRA_AUTO_RESTART_ACTION"; + public static final String EXTRA_AUTO_RESTART_ACTION = "EventStreamService.EXTRA_AUTO_RESTART_ACTION"; /** * Notification identifiers */ - private static final int NOTIF_ID_MESSAGE = 60; - private static final int NOTIF_ID_FOREGROUND_SERVICE = 61; + private static final int NOTIFICATION_ID = 123; + + public enum NotificationState { + // no notifications are displayed + NONE, + // initial sync in progress + INITIAL_SYNCING, + // fdroid mode or GCM registration failed + // put this service in foreground to keep it in life + LISTENING_FOR_EVENTS, + // display events notifications + DISPLAYING_EVENTS_NOTIFICATIONS, + // there is a pending incoming call + INCOMING_CALL, + // a call is in progress + CALL_IN_PROGRESS, + } - private static final int FOREGROUND_INITIAL_SYNCING = 41; - private static final int FOREGROUND_LISTENING_FOR_EVENTS = 42; - private static final int FOREGROUND_NOTIF_ID_PENDING_CALL = 44; - private static final int FOREGROUND_ID_INCOMING_CALL = 45; - private int mForegroundServiceIdentifier = -1; + private static NotificationState mNotificationState = NotificationState.NONE; /** * Default bing rule @@ -142,8 +156,8 @@ public enum StreamAction { /** * store the notifications description */ - private final LinkedHashMap mPendingNotifications = new LinkedHashMap<>(); - private Map> mNotifiedEventsByRoomId = null; + private final LinkedHashMap mPendingNotifications = new LinkedHashMap<>(); + private Map> mNotifiedEventsByRoomId = null; private static HandlerThread mNotificationHandlerThread = null; private static android.os.Handler mNotificationsHandler = null; @@ -426,7 +440,7 @@ private void autoRestart() { Log.d(LOG_TAG, "## autoRestart() : restarts after " + delay + " ms"); // reset the service identifier - mForegroundServiceIdentifier = -1; + mNotificationState = NotificationState.NONE; // restart the services after 3 seconds Intent restartServiceIntent = new Intent(getApplicationContext(), this.getClass()); @@ -457,11 +471,23 @@ public void onTaskRemoved(Intent rootIntent) { @Override public void onDestroy() { if (!mIsSelfDestroyed) { - Log.d(LOG_TAG, "## onDestroy() : restart it"); setServiceState(StreamAction.STOP); + + // stop the foreground service on devices which uses the battery optimisation + // during the initial syncing + // and if the GCM registration was done + if (PreferencesManager.useBatteryOptimisation(getApplicationContext()) && + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && + (mNotificationState == NotificationState.INITIAL_SYNCING) + && Matrix.getInstance(getApplicationContext()).getSharedGCMRegistrationManager().hasRegistrationToken()) { + stopForeground(true); + mIsForeground = false; + } + + Log.d(LOG_TAG, "## onDestroy() : restart it"); autoRestart(); } else { - Log.d(LOG_TAG, "## onDestroy()"); + Log.d(LOG_TAG, "## onDestroy() : do nothing"); stop(); super.onDestroy(); } @@ -536,7 +562,7 @@ public void run() { (new Handler(getMainLooper())).post(new Runnable() { @Override public void run() { - updateServiceForegroundState(); + refreshStatusNotification(); } }); } @@ -647,13 +673,20 @@ private void start() { monitorSession(session); } - if (!mGcmRegistrationManager.useGCM()) { - updateServiceForegroundState(); - } + refreshStatusNotification(); setServiceState(StreamAction.START); } + /** + * Stop the service without delay + */ + public void stopNow() { + stop(); + mIsSelfDestroyed = true; + stopSelf(); + } + /** * internal stop. */ @@ -761,24 +794,33 @@ private void gcmStatusUpdate() { Log.d(LOG_TAG, "## gcmStatusUpdate"); if (mIsForeground) { - Log.d(LOG_TAG, "## gcmStatusUpdate : gcm status succeeds so stopForeground"); - if (FOREGROUND_LISTENING_FOR_EVENTS == mForegroundServiceIdentifier) { + Log.d(LOG_TAG, "## gcmStatusUpdate : gcm status succeeds so stopForeground (" + mNotificationState + ")"); + + if (NotificationState.LISTENING_FOR_EVENTS == mNotificationState) { stopForeground(true); - mForegroundServiceIdentifier = -1; + mNotificationState = NotificationState.NONE; mIsForeground = false; } } - updateServiceForegroundState(); + refreshStatusNotification(); } /** - * Enable/disable the service foreground status. - * The service is put in foreground ("Foreground process priority") when a sync polling is used, - * to strongly reduce the likelihood of the App being killed. + * @return true if the "listen for events" notification should be displayed */ - private void updateServiceForegroundState() { - Log.d(LOG_TAG, "## updateServiceForegroundState"); + private boolean shouldDisplayListenForEventsNotification() { + // fdroid + return (!mGcmRegistrationManager.useGCM() || + // the GCM registration was not done + TextUtils.isEmpty(mGcmRegistrationManager.getCurrentRegistrationToken()) && !mGcmRegistrationManager.isServerRegistred()) && mGcmRegistrationManager.isBackgroundSyncAllowed() && mGcmRegistrationManager.areDeviceNotificationsAllowed(); + } + + /** + * Manages the "listen for events" and "synchronising" notifications + */ + public void refreshStatusNotification() { + Log.d(LOG_TAG, "## refreshStatusNotification from state " + mNotificationState); MXSession session = Matrix.getInstance(getApplicationContext()).getDefaultSession(); @@ -787,43 +829,70 @@ private void updateServiceForegroundState() { return; } + // call in progress notifications + if ((mNotificationState == NotificationState.INCOMING_CALL) || (mNotificationState == NotificationState.CALL_IN_PROGRESS)) { + Log.d(LOG_TAG, "## refreshStatusNotification : does nothing as there is a pending call"); + return; + } + + if (mNotificationState == NotificationState.DISPLAYING_EVENTS_NOTIFICATIONS) { + if (PreferencesManager.useBatteryOptimisation(getApplicationContext()) && + ((mServiceState == StreamAction.CATCHUP) || isStopped()) && !mIsForeground) { + if (mServiceState == StreamAction.CATCHUP) { + Log.d(LOG_TAG, "## refreshStatusNotification : events notif is displayed but the application is catchup up"); + } else { + Log.d(LOG_TAG, "## refreshStatusNotification : events notif is displayed but the service was stopped"); + } + mNotificationState = NotificationState.NONE; + } else { + Log.d(LOG_TAG, "## refreshStatusNotification : displaying events notification"); + } + return; + } + + if (mNotificationState == NotificationState.NONE) { + Notification notification = NotificationUtils.buildMessageNotification(getApplicationContext(), true); + + if (null != notification) { + mNotificationState = NotificationState.DISPLAYING_EVENTS_NOTIFICATIONS; + startForeground(NOTIFICATION_ID, notification); + mIsForeground = true; + Log.d(LOG_TAG, "## refreshStatusNotification : restore the events notification"); + return; + } + } + // GA issue if (null == mGcmRegistrationManager) { return; } - boolean isInitialSyncInProgress = !session.getDataHandler().isInitialSyncComplete(); + boolean isInitialSyncInProgress = !session.getDataHandler().isInitialSyncComplete() || isStopped() || (mServiceState == StreamAction.CATCHUP); if (isInitialSyncInProgress) { - Log.d(LOG_TAG, "## updateServiceForegroundState : put the service in foreground because of an initial sync"); + Log.d(LOG_TAG, "## refreshStatusNotification : put the service in foreground because of an initial sync " + mNotificationState); - if (FOREGROUND_INITIAL_SYNCING != mForegroundServiceIdentifier) { - Notification notification = buildForegroundServiceNotification(getString(R.string.notification_sync_in_progress)); - startForeground(NOTIF_ID_FOREGROUND_SERVICE, notification); - mForegroundServiceIdentifier = FOREGROUND_INITIAL_SYNCING; + if (mNotificationState != NotificationState.INITIAL_SYNCING) { + startForeground(NOTIFICATION_ID, buildForegroundServiceNotification(getString(R.string.notification_sync_in_progress))); + mNotificationState = NotificationState.INITIAL_SYNCING; } - mIsForeground = true; - } else if ( - // fdroid - (!mGcmRegistrationManager.useGCM() || - // the GCM registration was not done - TextUtils.isEmpty(mGcmRegistrationManager.getCurrentRegistrationToken()) && !mGcmRegistrationManager.isServerRegistred()) && mGcmRegistrationManager.isBackgroundSyncAllowed() && mGcmRegistrationManager.areDeviceNotificationsAllowed()) { - Log.d(LOG_TAG, "## updateServiceForegroundState : put the service in foreground because of GCM registration"); - - if (FOREGROUND_LISTENING_FOR_EVENTS != mForegroundServiceIdentifier) { - Notification notification = buildForegroundServiceNotification(getString(R.string.notification_listen_for_events)); - startForeground(NOTIF_ID_FOREGROUND_SERVICE, notification); - mForegroundServiceIdentifier = FOREGROUND_LISTENING_FOR_EVENTS; + } else if (shouldDisplayListenForEventsNotification()) { + Log.d(LOG_TAG, "## refreshStatusNotification : put the service in foreground because of GCM registration"); + + if (mNotificationState != NotificationState.LISTENING_FOR_EVENTS) { + startForeground(NOTIFICATION_ID, buildForegroundServiceNotification(getString(R.string.notification_listen_for_events))); + mNotificationState = NotificationState.LISTENING_FOR_EVENTS; } mIsForeground = true; } else { - Log.d(LOG_TAG, "## updateServiceForegroundState : put the service in background"); - - if ((FOREGROUND_LISTENING_FOR_EVENTS == mForegroundServiceIdentifier) || (FOREGROUND_INITIAL_SYNCING == mForegroundServiceIdentifier)) { + if ((mNotificationState == NotificationState.LISTENING_FOR_EVENTS) || (mNotificationState == NotificationState.INITIAL_SYNCING)) { + Log.d(LOG_TAG, "## refreshStatusNotification : put the service in background from state " + mNotificationState); stopForeground(true); - mForegroundServiceIdentifier = -1; + mNotificationState = NotificationState.NONE; + } else { + Log.d(LOG_TAG, "## refreshStatusNotification : nothing to do"); } mIsForeground = false; } @@ -987,7 +1056,7 @@ private void prepareNotification(Event event, BingRule bingRule) { bingRule = mDefaultBingRule; } - mPendingNotifications.put(event.eventId, new NotificationUtils.NotifiedEvent(event.roomId, event.eventId, bingRule, event.getOriginServerTs())); + mPendingNotifications.put(event.eventId, new NotifiedEvent(event.roomId, event.eventId, bingRule, event.getOriginServerTs())); } /** @@ -1070,6 +1139,8 @@ public void run() { if (null != mNotifiedEventsByRoomId) { mNotifiedEventsByRoomId.clear(); } + + RoomsNotifications.deleteCachedRoomNotifications(VectorApp.getInstance()); } }); } @@ -1132,18 +1203,27 @@ public static void onStaticNotifiedEvent(Context context, Event event, String ro if ((null != event) && !mBackgroundNotificationEventIds.contains(event.eventId)) { mBackgroundNotificationEventIds.add(event.eventId); - - String header = (TextUtils.isEmpty(roomName) ? event.roomId : roomName) + ": " + - (TextUtils.isEmpty(senderDisplayName) ? event.sender : senderDisplayName) + " "; - + String header = ""; String text; - if (event.isEncrypted()) { - text = context.getString(R.string.encrypted_message); + if (null == event.content) { + if (1 == mBackgroundNotificationEventIds.size()) { + text = context.getString(R.string.one_new_message); + } else { + text = context.getString(R.string.new_messages, mBackgroundNotificationEventIds.size()); + } + mBackgroundNotificationStrings.clear(); } else { - EventDisplay eventDisplay = new RiotEventDisplay(context, event, null); - eventDisplay.setPrependMessagesWithAuthor(false); - text = eventDisplay.getTextualDisplay().toString(); + header = (TextUtils.isEmpty(roomName) ? event.roomId : roomName) + ": " + + (TextUtils.isEmpty(senderDisplayName) ? event.sender : senderDisplayName) + " "; + + if (event.isEncrypted()) { + text = context.getString(R.string.encrypted_message); + } else { + EventDisplay eventDisplay = new RiotEventDisplay(context, event, null); + eventDisplay.setPrependMessagesWithAuthor(false); + text = eventDisplay.getTextualDisplay().toString(); + } } if (!TextUtils.isEmpty(text)) { @@ -1154,117 +1234,38 @@ public static void onStaticNotifiedEvent(Context context, Event event, String ro Notification notification = NotificationUtils.buildMessagesListNotification(context, mBackgroundNotificationStrings, new BingRule(null, null, true, true, true)); if (null != notification) { - nm.notify(NOTIF_ID_MESSAGE, notification); + nm.notify(NOTIFICATION_ID, notification); + mNotificationState = NotificationState.DISPLAYING_EVENTS_NOTIFICATIONS; } else { - nm.cancel(NOTIF_ID_MESSAGE); + nm.cancel(NOTIFICATION_ID); + mNotificationState = NotificationState.NONE; } } } else if (0 == unreadMessagesCount) { mBackgroundNotificationStrings.clear(); - nm.cancel(NOTIF_ID_MESSAGE); + nm.cancel(NOTIFICATION_ID); + mNotificationState = NotificationState.NONE; } } /** - * Notify that a notification for even has been received. + * Dismiss the messages notifications. * - * @param event the notified event - * @param roomName the room name - * @param senderDisplayName the sender display name - * @param unreadMessagesCount the unread messages count + * @param nm the notifications manager */ - public void onNotifiedEventWithBackgroundSyncDisabled(Event event, String roomName, String senderDisplayName, int unreadMessagesCount) { - if ((null != event) && !mBackgroundNotificationEventIds.contains(event.eventId)) { - mBackgroundNotificationEventIds.add(event.eventId); - - // TODO the session id should be provided by the server - MXSession session = Matrix.getInstance(getApplicationContext()).getDefaultSession(); - - if (null != session) { - RoomState roomState = null; - - try { - roomState = session.getDataHandler().getRoom(event.roomId).getLiveState(); - } catch (Exception e) { - Log.e(LOG_TAG, "Fail to retrieve the roomState of " + event.roomId); - } - - if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTED) && session.isCryptoEnabled()) { - session.getDataHandler().decryptEvent(event, null); - } - - // test if the message is displayable - EventDisplay eventDisplay = new RiotEventDisplay(getApplicationContext(), event, roomState); - eventDisplay.setPrependMessagesWithAuthor(false); - String text = eventDisplay.getTextualDisplay().toString(); - - // display a dedicated message in decryption error cases - if (null != event.getCryptoError()) { - text = getApplicationContext().getString(R.string.encrypted_message); - } - - // sanity check - if (!TextUtils.isEmpty(text) && (null != roomState)) { - - if (TextUtils.isEmpty(roomName)) { - roomName = roomState.getDisplayName(session.getMyUserId()); - } - - if (TextUtils.isEmpty(senderDisplayName)) { - senderDisplayName = roomState.getMemberName(event.sender); - } - - String header = roomName + ": " + senderDisplayName + " "; - - SpannableString notifiedLine = new SpannableString(header + text); - notifiedLine.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, header.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - Log.d(LOG_TAG, "## onMessageReceivedInternal() : trigger a notification " + notifiedLine); - - mBackgroundNotificationStrings.add(0, notifiedLine); - BingRulesManager bingRulesManager = session.getDataHandler().getBingRulesManager(); - BingRule rule = bingRulesManager.isReady() ? bingRulesManager.fulfilledBingRule(event) : new BingRule(false); - - displayMessagesNotification(mBackgroundNotificationStrings, rule); - } + private void dismissMessagesNotification(NotificationManagerCompat nm) { + if (mNotificationState == NotificationState.DISPLAYING_EVENTS_NOTIFICATIONS) { + Log.d(LOG_TAG, "## dismissMessagesNotification() : clear notification"); + if (mIsForeground) { + stopForeground(true); + mIsForeground = false; + } else { + nm.cancel(NOTIFICATION_ID); } - } else if (0 == unreadMessagesCount) { - mBackgroundNotificationStrings.clear(); - displayMessagesNotification(mBackgroundNotificationStrings, null); - } - } - - /** - * Display a list of events as string. - * - * @param messages the messages list - * @param rule the bing rule to use - */ - private void displayMessagesNotification(final List messages, final BingRule rule) { - NotificationUtils.addNotificationChannels(this); - final NotificationManagerCompat nm = NotificationManagerCompat.from(EventStreamService.this); - - if (!mGcmRegistrationManager.areDeviceNotificationsAllowed() || (null == messages) || (0 == messages.size())) { - new Handler(getMainLooper()).post(new Runnable() { - @Override - public void run() { - nm.cancel(NOTIF_ID_MESSAGE); - } - }); - } else { - new Handler(getMainLooper()).post(new Runnable() { - @Override - public void run() { - Notification notif = NotificationUtils.buildMessagesListNotification(getApplicationContext(), messages, rule); - if (null != notif) { - nm.notify(NOTIF_ID_MESSAGE, notif); - - } else { - nm.cancel(NOTIF_ID_MESSAGE); - } - } - }); + mNotificationState = NotificationState.NONE; + RoomsNotifications.deleteCachedRoomNotifications(getApplicationContext()); + refreshStatusNotification(); } } @@ -1279,13 +1280,13 @@ private void refreshMessagesNotification() { final NotificationManagerCompat nm = NotificationManagerCompat.from(EventStreamService.this); - NotificationUtils.NotifiedEvent eventToNotify = getEventToNotify(); + NotifiedEvent eventToNotify = getEventToNotify(); if (!mGcmRegistrationManager.areDeviceNotificationsAllowed()) { mNotifiedEventsByRoomId = null; new Handler(getMainLooper()).post(new Runnable() { @Override public void run() { - nm.cancel(NOTIF_ID_MESSAGE); + dismissMessagesNotification(nm); } }); } else if (refreshNotifiedMessagesList()) { @@ -1294,7 +1295,7 @@ public void run() { new Handler(getMainLooper()).post(new Runnable() { @Override public void run() { - nm.cancel(NOTIF_ID_MESSAGE); + dismissMessagesNotification(nm); } }); } else { @@ -1316,8 +1317,8 @@ public void run() { // search the latest message to refresh the notification for (String roomId : roomIds) { - List events = mNotifiedEventsByRoomId.get(roomId); - NotificationUtils.NotifiedEvent notifiedEvent = events.get(events.size() - 1); + List events = mNotifiedEventsByRoomId.get(roomId); + NotifiedEvent notifiedEvent = events.get(events.size() - 1); Event event = store.getEvent(notifiedEvent.mEventId, notifiedEvent.mRoomId); @@ -1332,8 +1333,8 @@ public void run() { } } - final NotificationUtils.NotifiedEvent fEventToNotify = eventToNotify; - final Map> fNotifiedEventsByRoomId = new HashMap<>(mNotifiedEventsByRoomId); + final NotifiedEvent fEventToNotify = eventToNotify; + final Map> fNotifiedEventsByRoomId = new HashMap<>(mNotifiedEventsByRoomId); if (null != fEventToNotify) { DismissNotificationReceiver.setLatestNotifiedMessageTs(this, fEventToNotify.mOriginServerTs); @@ -1351,13 +1352,25 @@ public void run() { // the notification cannot be built if (null != notif) { - nm.notify(NOTIF_ID_MESSAGE, notif); + if (shouldDisplayListenForEventsNotification()) { + mIsForeground = true; + startForeground(NOTIFICATION_ID, notif); + } else { + if (mIsForeground) { + stopForeground(true); + mIsForeground = false; + } + nm.notify(NOTIFICATION_ID, notif); + } + mNotificationState = NotificationState.DISPLAYING_EVENTS_NOTIFICATIONS; + Log.d(LOG_TAG, "## refreshMessagesNotification() : display the notification"); } else { - nm.cancel(NOTIF_ID_MESSAGE); + Log.d(LOG_TAG, "## refreshMessagesNotification() : nothing to display"); + dismissMessagesNotification(nm); } } else { Log.e(LOG_TAG, "## refreshMessagesNotification() : mNotifiedEventsByRoomId is empty"); - nm.cancel(NOTIF_ID_MESSAGE); + dismissMessagesNotification(nm); } } }); @@ -1369,18 +1382,18 @@ public void run() { * Check if the current displayed notification must be cleared * because it doesn't make sense anymore. */ - private NotificationUtils.NotifiedEvent getEventToNotify() { + private NotifiedEvent getEventToNotify() { if (mPendingNotifications.size() > 0) { // TODO add multi sessions MXSession session = Matrix.getInstance(getBaseContext()).getDefaultSession(); IMXStore store = session.getDataHandler().getStore(); // notified only the latest unread message - List eventsToNotify = new ArrayList<>(mPendingNotifications.values()); + List eventsToNotify = new ArrayList<>(mPendingNotifications.values()); Collections.reverse(eventsToNotify); - for (NotificationUtils.NotifiedEvent eventToNotify : eventsToNotify) { + for (NotifiedEvent eventToNotify : eventsToNotify) { Room room = store.getRoom(eventToNotify.mRoomId); // test if the message has not been read @@ -1467,8 +1480,8 @@ private boolean refreshNotifiedMessagesList() { BingRule rule = session.fulfillRule(event); if ((null != rule) && rule.isEnabled && rule.shouldNotify()) { - List list = new ArrayList<>(); - list.add(new NotificationUtils.NotifiedEvent(event.roomId, event.eventId, rule, event.getOriginServerTs())); + List list = new ArrayList<>(); + list.add(new NotifiedEvent(event.roomId, event.eventId, rule, event.getOriginServerTs())); mNotifiedEventsByRoomId.put(room.getRoomId(), list); } } @@ -1483,14 +1496,14 @@ private boolean refreshNotifiedMessagesList() { List unreadEvents = store.unreadEvents(room.getRoomId(), null); if ((null != unreadEvents) && unreadEvents.size() > 0) { - List list = new ArrayList<>(); + List list = new ArrayList<>(); for (Event event : unreadEvents) { if (event.getOriginServerTs() > minTs) { BingRule rule = session.fulfillRule(event); if ((null != rule) && rule.isEnabled && rule.shouldNotify()) { - list.add(new NotificationUtils.NotifiedEvent(event.roomId, event.eventId, rule, event.getOriginServerTs())); + list.add(new NotifiedEvent(event.roomId, event.eventId, rule, event.getOriginServerTs())); //Log.d(LOG_TAG, "## refreshNotifiedMessagesList() : the event " + event.eventId + " in room " + event.roomId + " fulfills " + rule); } } else { @@ -1525,20 +1538,20 @@ private boolean refreshNotifiedMessagesList() { isUpdated = true; } else { // the messages are sorted from the oldest to the latest - List events = mNotifiedEventsByRoomId.get(roomId); + List events = mNotifiedEventsByRoomId.get(roomId); // if the oldest event has been read // something has been updated - NotificationUtils.NotifiedEvent oldestEvent = events.get(0); + NotifiedEvent oldestEvent = events.get(0); if (room.isEventRead(oldestEvent.mEventId) || (oldestEvent.mOriginServerTs < minTs)) { // if the latest message has been read // we have to find out the unread messages - NotificationUtils.NotifiedEvent latestEvent = events.get(events.size() - 1); + NotifiedEvent latestEvent = events.get(events.size() - 1); if (!room.isEventRead(latestEvent.mEventId) && latestEvent.mOriginServerTs > minTs) { // search for the read messages for (int i = 0; i < events.size(); ) { - NotificationUtils.NotifiedEvent event = events.get(i); + NotifiedEvent event = events.get(i); if (room.isEventRead(event.mEventId) || (event.mOriginServerTs <= minTs)) { // Log.d(LOG_TAG, "## refreshNotifiedMessagesList() : the event " + event.mEventId + " in room " + room.getRoomId() + " is read"); @@ -1597,12 +1610,14 @@ else if (null == CallsManager.getSharedInstance().getActiveCall()) { Log.d(LOG_TAG, "displayIncomingCallNotification : display the dedicated notification"); Notification notification = NotificationUtils.buildIncomingCallNotification( EventStreamService.this, - NotificationUtils.getRoomName(getApplicationContext(), session, room, event), + RoomsNotifications.getRoomName(getApplicationContext(), session, room, event), session.getMyUserId(), callId); - startForeground(NOTIF_ID_FOREGROUND_SERVICE, notification); - mForegroundServiceIdentifier = FOREGROUND_ID_INCOMING_CALL; + + startForeground(NOTIFICATION_ID, notification); + mNotificationState = NotificationState.INCOMING_CALL; + mIsForeground = true; mIncomingCallId = callId; @@ -1619,7 +1634,7 @@ else if (null == CallsManager.getSharedInstance().getActiveCall()) { } /** - * Display a call in progress notificatin. + * Display a call in progress notification. * * @param session the session * @param callId the callId @@ -1627,8 +1642,8 @@ else if (null == CallsManager.getSharedInstance().getActiveCall()) { public void displayCallInProgressNotification(MXSession session, Room room, String callId) { if (null != callId) { Notification notification = NotificationUtils.buildPendingCallNotification(getApplicationContext(), room.getName(session.getCredentials().userId), room.getRoomId(), session.getCredentials().userId, callId); - startForeground(NOTIF_ID_FOREGROUND_SERVICE, notification); - mForegroundServiceIdentifier = FOREGROUND_NOTIF_ID_PENDING_CALL; + startForeground(NOTIFICATION_ID, notification); + mNotificationState = NotificationState.CALL_IN_PROGRESS; mCallIdInProgress = callId; } } @@ -1640,16 +1655,17 @@ public void hideCallNotifications() { NotificationManager nm = (NotificationManager) EventStreamService.this.getSystemService(Context.NOTIFICATION_SERVICE); // hide the call - if ((FOREGROUND_NOTIF_ID_PENDING_CALL == mForegroundServiceIdentifier) || (FOREGROUND_ID_INCOMING_CALL == mForegroundServiceIdentifier)) { - if (FOREGROUND_NOTIF_ID_PENDING_CALL == mForegroundServiceIdentifier) { + if ((NotificationState.CALL_IN_PROGRESS == mNotificationState) || (NotificationState.INCOMING_CALL == mNotificationState)) { + if (NotificationState.CALL_IN_PROGRESS == mNotificationState) { mCallIdInProgress = null; } else { mIncomingCallId = null; } - nm.cancel(NOTIF_ID_FOREGROUND_SERVICE); - mForegroundServiceIdentifier = -1; + nm.cancel(NOTIFICATION_ID); stopForeground(true); - updateServiceForegroundState(); + + mNotificationState = NotificationState.NONE; + refreshStatusNotification(); } } } diff --git a/vector/src/main/java/im/vector/util/BugReporter.java b/vector/src/main/java/im/vector/util/BugReporter.java index c4bedafb48..09c7f6a3fc 100755 --- a/vector/src/main/java/im/vector/util/BugReporter.java +++ b/vector/src/main/java/im/vector/util/BugReporter.java @@ -300,6 +300,7 @@ public void onWrite(long totalWritten, long contentLength) { int responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR; Response response = null; + String errorMessage = null; // trigger the request try { @@ -308,11 +309,14 @@ public void onWrite(long totalWritten, long contentLength) { responseCode = response.code(); } catch (Exception e) { Log.e(LOG_TAG, "response " + e.getMessage()); + errorMessage = e.getLocalizedMessage(); } // if the upload failed, try to retrieve the reason if (responseCode != HttpURLConnection.HTTP_OK) { - if ((null == response) || (null == response.body())) { + if (null != errorMessage) { + serverError = "Failed with error " + errorMessage; + } else if ((null == response) || (null == response.body())) { serverError = "Failed with error " + responseCode; } else { InputStream is = null; diff --git a/vector/src/main/java/im/vector/util/CallsManager.java b/vector/src/main/java/im/vector/util/CallsManager.java index 6927f5e812..7bc405954f 100755 --- a/vector/src/main/java/im/vector/util/CallsManager.java +++ b/vector/src/main/java/im/vector/util/CallsManager.java @@ -22,6 +22,7 @@ import android.media.AudioManager; import android.os.Handler; import android.os.Looper; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.view.View; import android.widget.Toast; @@ -356,9 +357,22 @@ public void onIncomingCall(final IMXCall aCall, final MXUsersDevicesMap launch it"); - Context context = VectorApp.getInstance(); - // clear the activity stack to home activity Intent intent = new Intent(context, VectorHomeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/vector/src/main/java/im/vector/util/GroupUtils.java b/vector/src/main/java/im/vector/util/GroupUtils.java new file mode 100644 index 0000000000..0adc83608c --- /dev/null +++ b/vector/src/main/java/im/vector/util/GroupUtils.java @@ -0,0 +1,198 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.util; + +import android.app.Activity; +import android.content.Intent; +import android.text.TextUtils; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.data.Room; +import org.matrix.androidsdk.data.RoomPreviewData; +import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.callback.SimpleApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.rest.model.group.GroupRoom; +import org.matrix.androidsdk.rest.model.group.GroupUser; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import im.vector.activity.CommonActivityUtils; +import im.vector.activity.VectorMemberDetailsActivity; +import im.vector.activity.VectorRoomActivity; + +public class GroupUtils { + private static final String LOG_TAG = GroupUtils.class.getSimpleName(); + + /** + * Create a list of groups by filtering the given list with the given pattern + * + * @param groupsToFilter + * @param constraint + * @return filtered groups + */ + public static List getFilteredGroups(final List groupsToFilter, final CharSequence constraint) { + final String filterPattern = constraint != null ? constraint.toString().trim() : null; + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroups = new ArrayList<>(); + Pattern pattern = Pattern.compile(Pattern.quote(filterPattern), Pattern.CASE_INSENSITIVE); + + for (final Group group : groupsToFilter) { + if (pattern.matcher(group.getDisplayName()).find()) { + filteredGroups.add(group); + } + } + return filteredGroups; + } else { + return groupsToFilter; + } + } + + /** + * Create a list of groups by filtering the given list with the given pattern + * + * @param groupsUsersToFilter + * @param constraint + * @return filtered group users + */ + public static List getFilteredGroupUsers(final List groupsUsersToFilter, final CharSequence constraint) { + final String filterPattern = constraint != null ? constraint.toString().trim() : null; + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroupUsers = new ArrayList<>(); + Pattern pattern = Pattern.compile(Pattern.quote(filterPattern), Pattern.CASE_INSENSITIVE); + + for (final GroupUser groupUser : groupsUsersToFilter) { + if (pattern.matcher(groupUser.getDisplayname()).find()) { + filteredGroupUsers.add(groupUser); + } + } + return filteredGroupUsers; + } else { + return groupsUsersToFilter; + } + } + + /** + * Create a list of groups by filtering the given list with the given pattern + * + * @param groupRoomsToFilter + * @param constraint + * @return filtered group users + */ + public static List getFilteredGroupRooms(final List groupRoomsToFilter, final CharSequence constraint) { + final String filterPattern = constraint != null ? constraint.toString().trim() : null; + if (!TextUtils.isEmpty(filterPattern)) { + List filteredGroupRooms = new ArrayList<>(); + Pattern pattern = Pattern.compile(Pattern.quote(filterPattern), Pattern.CASE_INSENSITIVE); + + for (final GroupRoom groupRoom : groupRoomsToFilter) { + if (pattern.matcher(groupRoom.getDisplayName()).find()) { + filteredGroupRooms.add(groupRoom); + } + } + return filteredGroupRooms; + } else { + return groupRoomsToFilter; + } + } + + /** + * Open the detailed group user page + * + * @param fromActivity the caller activity + * @param session the session + * @param groupUser the group user + */ + public static void openGroupUserPage(Activity fromActivity, MXSession session, GroupUser groupUser) { + Intent userIntent = new Intent(fromActivity, VectorMemberDetailsActivity.class); + userIntent.putExtra(VectorMemberDetailsActivity.EXTRA_MEMBER_ID, groupUser.userId); + + if (!TextUtils.isEmpty(groupUser.avatarUrl)) { + userIntent.putExtra(VectorMemberDetailsActivity.EXTRA_MEMBER_AVATAR_URL, groupUser.avatarUrl); + } + + if (!TextUtils.isEmpty(groupUser.displayname)) { + userIntent.putExtra(VectorMemberDetailsActivity.EXTRA_MEMBER_DISPLAY_NAME, groupUser.displayname); + } + + userIntent.putExtra(VectorMemberDetailsActivity.EXTRA_MATRIX_ID, session.getCredentials().userId); + fromActivity.startActivity(userIntent); + } + + /** + * Open the detailed group room page + * + * @param fromActivity the caller activity + * @param session the session + * @param groupRoom the group room + */ + public static void openGroupRoom(final Activity fromActivity, final MXSession session, final GroupRoom groupRoom, final SimpleApiCallback callback) { + Room room = session.getDataHandler().getStore().getRoom(groupRoom.roomId); + + if ((null == room) || (null == room.getMember(session.getMyUserId()))) { + final RoomPreviewData roomPreviewData = new RoomPreviewData(session, groupRoom.roomId, null, groupRoom.getAlias(), null); + + roomPreviewData.fetchPreviewData(new ApiCallback() { + private void onDone() { + if (null != callback) { + callback.onSuccess(null); + } + + CommonActivityUtils.previewRoom(fromActivity, roomPreviewData); + } + + @Override + public void onSuccess(Void info) { + onDone(); + } + + private void onError() { + roomPreviewData.setRoomState(groupRoom); + roomPreviewData.setRoomName(groupRoom.name); + onDone(); + } + + @Override + public void onNetworkError(Exception e) { + onError(); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(); + } + }); + } else { + Intent roomIntent = new Intent(fromActivity, VectorRoomActivity.class); + roomIntent.putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, session.getMyUserId()); + roomIntent.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, groupRoom.roomId); + fromActivity.startActivity(roomIntent); + + if (null != callback) { + callback.onSuccess(null); + } + } + } +} diff --git a/vector/src/main/java/im/vector/util/MatrixURLSpan.java b/vector/src/main/java/im/vector/util/MatrixURLSpan.java index 9da5473b6e..ec9444002e 100755 --- a/vector/src/main/java/im/vector/util/MatrixURLSpan.java +++ b/vector/src/main/java/im/vector/util/MatrixURLSpan.java @@ -130,6 +130,10 @@ public void onClick(View widget) { if (null != mActionsListener) { mActionsListener.onMessageIdClick(mURL); } + } else if (mPattern == MXSession.PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER) { + if (null != mActionsListener) { + mActionsListener.onGroupIdClick(mURL); + } } else { Uri uri = Uri.parse(getURL()); @@ -156,7 +160,8 @@ public void onClick(View widget) { MXSession.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, MXSession.PATTERN_CONTAIN_MATRIX_ALIAS, MXSession.PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, - MXSession.PATTERN_CONTAIN_MATRIX_MESSAGE_IDENTIFIER + MXSession.PATTERN_CONTAIN_MATRIX_MESSAGE_IDENTIFIER, + MXSession.PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER ); /** diff --git a/vector/src/main/java/im/vector/util/PhoneNumberUtils.java b/vector/src/main/java/im/vector/util/PhoneNumberUtils.java index 6e8e3e3eed..44f6f3ff82 100755 --- a/vector/src/main/java/im/vector/util/PhoneNumberUtils.java +++ b/vector/src/main/java/im/vector/util/PhoneNumberUtils.java @@ -23,7 +23,6 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; -import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; @@ -160,7 +159,7 @@ public static String getCountryCode(final Context context) { if (!preferences.contains(COUNTRY_CODE_PREF_KEY) || TextUtils.isEmpty(preferences.getString(COUNTRY_CODE_PREF_KEY, ""))) { try { TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - String countryCode = tm.getNetworkCountryIso().toUpperCase(); + String countryCode = tm.getNetworkCountryIso().toUpperCase(VectorApp.getApplicationLocale()); if (TextUtils.isEmpty(countryCode) && !TextUtils.isEmpty(Locale.getDefault().getCountry()) && PhoneNumberUtil.getInstance().getCountryCodeForRegion(Locale.getDefault().getCountry()) != 0) { diff --git a/vector/src/main/java/im/vector/util/PreferencesManager.java b/vector/src/main/java/im/vector/util/PreferencesManager.java index 263fcdb1aa..3acc21e259 100755 --- a/vector/src/main/java/im/vector/util/PreferencesManager.java +++ b/vector/src/main/java/im/vector/util/PreferencesManager.java @@ -18,7 +18,6 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.media.RingtoneManager; @@ -121,8 +120,17 @@ public class PreferencesManager { private static final String SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY"; public static final String SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY"; + public static final String SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY"; + private static final String SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY"; + public static final String SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"; + + private static final String SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY"; + + private static final String SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"; + + private static final String SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"; private static final int MEDIA_SAVING_3_DAYS = 0; private static final int MEDIA_SAVING_1_WEEK = 1; @@ -193,18 +201,18 @@ public static void clearPreferences(Context context) { } /** - * Tells if a background service can be started. + * Tells if the battery optimisations are ignored for this application. * * @param context the context - * @return true if a background service can be started. + * @return true if the battery optimisations are ignored. */ @SuppressLint("NewApi") - public static boolean canStartBackgroundService(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return ((PowerManager) context.getSystemService(context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName()); + public static boolean useBatteryOptimisation(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return !((PowerManager) context.getSystemService(context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName()); } - return true; + return false; } /** @@ -606,9 +614,52 @@ public static boolean pinUnreadMessages(Context context) { * Tells if Piwik can be used * * @param context the context - * @return null if not defined, true / false when defined + * @return true to use it */ - public static Boolean trackWithPiwik(Context context) { + public static boolean trackWithPiwik(Context context) { return !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISABLE_PIWIK_SETTINGS_PREFERENCE_KEY, false); } + + /** + * Tells if the phone must vibrate when mentioning + * + * @param context the context + * @return true + */ + public static boolean vibrateWhenMentioning(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false); + } + + /** + * Tells if the rage shake is used. + * + * @param context the context + * @return true if the rage shake is used + */ + public static boolean useRageshake(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true); + } + + /** + * Update the rage shake status. + * + * @param context the context + * @param isEnabled true to enable the rage shake + */ + public static void setUseRageshake(Context context, boolean isEnabled) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled); + editor.commit(); + } + + /** + * Tells if all the events must be displayed ie even the redacted events. + * + * @param context the context + * @return true to display all the events even the redacted ones. + */ + public static boolean displayAllEvents(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false); + } } diff --git a/vector/src/main/java/im/vector/util/RageShake.java b/vector/src/main/java/im/vector/util/RageShake.java index bb5989f1e5..e6a4c0d9d1 100755 --- a/vector/src/main/java/im/vector/util/RageShake.java +++ b/vector/src/main/java/im/vector/util/RageShake.java @@ -41,10 +41,28 @@ public class RageShake implements SensorEventListener { // the context private Context mContext; + // the sensor + private SensorManager mSensorManager; + private Sensor mSensor; + private boolean mIsStarted; + /** * Constructor */ - public RageShake() { + public RageShake(Context context) { + mContext = context; + + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + + if (null != mSensorManager) { + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + if (null == mSensor) { + Log.e(LOG_TAG, "No accelerometer in this device. Cannot use rage shake."); + mSensorManager = null; + } + // Samsung devices for some reason seem to be less sensitive than others so the threshold is being // lowered for them. A possible lead for a better formula is the fact that the sensitivity detected // with the calculated force below seems to relate to the sample rate: The higher the sample rate, @@ -79,12 +97,7 @@ public void onClick(DialogInterface dialog, int which) { .setNeutralButton(R.string.disable, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); - - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(mContext.getString(im.vector.R.string.settings_key_use_rage_shake), false); - editor.commit(); - + PreferencesManager.setUseRageshake(mContext, false); dialog.dismiss(); } }) @@ -101,19 +114,27 @@ public void onClick(DialogInterface dialog, int which) { } } + /** * start the sensor detector */ - public void start(Context context) { - mContext = context; + public void start() { + if ((null != mSensorManager) && PreferencesManager.useRageshake(mContext) && !VectorApp.isAppInBackground() && !mIsStarted) { + mIsStarted = true; + mLastUpdate = 0; + mLastShake = 0; + mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + } - SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - Sensor s = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - if (s == null) { - Log.e(LOG_TAG, "No accelerometer in this device. Cannot use rage shake."); - return; + /** + * Stop the sensor detector + */ + public void stop() { + if (null != mSensorManager) { + mSensorManager.unregisterListener(this, mSensor); } - sm.registerListener(this, s, SensorManager.SENSOR_DELAY_NORMAL); + mIsStarted = false; } @Override @@ -136,12 +157,6 @@ public void onAccuracyChanged(Sensor sensor, int accuracy) { @Override public void onSensorChanged(SensorEvent event) { - // ignore the sensor events when the application is in background - if (VectorApp.isAppInBackground()) { - mLastUpdate = 0; - return; - } - if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) { return; } @@ -169,9 +184,7 @@ public void onSensorChanged(SensorEvent event) { Log.d(LOG_TAG, "Shaking detected."); mLastShakeTimestamp = System.currentTimeMillis(); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); - - if (preferences.getBoolean(mContext.getString(im.vector.R.string.settings_key_use_rage_shake), true)) { + if (PreferencesManager.useRageshake(mContext)) { promptForReport(); } } else { diff --git a/vector/src/main/java/im/vector/util/RoomUtils.java b/vector/src/main/java/im/vector/util/RoomUtils.java index 0a9b7384d5..bd54cb8ce0 100644 --- a/vector/src/main/java/im/vector/util/RoomUtils.java +++ b/vector/src/main/java/im/vector/util/RoomUtils.java @@ -20,12 +20,20 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; import android.os.Build; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.MenuItem; import android.view.View; +import android.widget.ImageView; import android.widget.PopupMenu; import org.matrix.androidsdk.MXSession; @@ -36,11 +44,13 @@ import org.matrix.androidsdk.data.store.IMXStore; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.Event; +import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.BingRulesManager; import org.matrix.androidsdk.util.EventDisplay; import org.matrix.androidsdk.util.Log; +import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; @@ -53,6 +63,7 @@ import im.vector.Matrix; import im.vector.R; import im.vector.activity.CommonActivityUtils; +import im.vector.activity.VectorRoomActivity; import im.vector.adapters.AdapterUtils; public class RoomUtils { @@ -60,7 +71,7 @@ public class RoomUtils { private static final String LOG_TAG = RoomUtils.class.getSimpleName(); public interface MoreActionListener { - void onToggleRoomNotifications(MXSession session, String roomId); + void onUpdateRoomNotificationsState(MXSession session, String roomId, BingRulesManager.RoomNotificationState state); void onToggleDirectChat(MXSession session, String roomId); @@ -71,6 +82,10 @@ public interface MoreActionListener { void moveToLowPriority(MXSession session, String roomId); void onLeaveRoom(MXSession session, String roomId); + + void onForgetRoom(MXSession session, String roomId); + + void addHomeScreenShortcut(MXSession session, String roomId); } public interface HistoricalRoomActionListener { @@ -450,18 +465,21 @@ private static void displayPopupMenu(final Context context, final MXSession sess return; } + Context popmenuContext = new ContextThemeWrapper(context, R.style.PopMenuStyle); + final PopupMenu popup; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - popup = new PopupMenu(context, actionView, Gravity.END); + popup = new PopupMenu(popmenuContext, actionView, Gravity.END); } else { - popup = new PopupMenu(context, actionView); + popup = new PopupMenu(popmenuContext, actionView); } popup.getMenuInflater().inflate(R.menu.vector_home_room_settings, popup.getMenu()); CommonActivityUtils.tintMenuIcons(popup.getMenu(), ThemeUtils.getColor(context, R.attr.settings_icon_tint_color)); if (room.isLeft()) { popup.getMenu().setGroupVisible(R.id.active_room_actions, false); + popup.getMenu().setGroupVisible(R.id.add_shortcut_actions, false); popup.getMenu().setGroupVisible(R.id.historical_room_actions, true); if (historicalRoomActionListener != null) { @@ -477,42 +495,92 @@ public boolean onMenuItemClick(final MenuItem item) { } } else { popup.getMenu().setGroupVisible(R.id.active_room_actions, true); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + popup.getMenu().setGroupVisible(R.id.add_shortcut_actions, false); + } else { + ShortcutManager manager = context.getSystemService(ShortcutManager.class); + + if (!manager.isRequestPinShortcutSupported()) { + popup.getMenu().setGroupVisible(R.id.add_shortcut_actions, false); + } else { + popup.getMenu().setGroupVisible(R.id.add_shortcut_actions, true); + } + } + popup.getMenu().setGroupVisible(R.id.historical_room_actions, false); MenuItem item; - final BingRulesManager bingRulesManager = session.getDataHandler().getBingRulesManager(); + BingRulesManager.RoomNotificationState state = session.getDataHandler().getBingRulesManager().getRoomNotificationState(room.getRoomId()); + + if (BingRulesManager.RoomNotificationState.ALL_MESSAGES_NOISY != state) { + item = popup.getMenu().findItem(R.id.ic_action_notifications_noisy); + item.setIcon(null); + } + + if (BingRulesManager.RoomNotificationState.ALL_MESSAGES != state) { + item = popup.getMenu().findItem(R.id.ic_action_notifications_all_message); + item.setIcon(null); + } + + if (BingRulesManager.RoomNotificationState.MENTIONS_ONLY != state) { + item = popup.getMenu().findItem(R.id.ic_action_notifications_mention_only); + item.setIcon(null); + } - if (bingRulesManager.isRoomNotificationsDisabled(room.getRoomId())) { - item = popup.getMenu().getItem(0); + if (BingRulesManager.RoomNotificationState.MUTE != state) { + item = popup.getMenu().findItem(R.id.ic_action_notifications_mute); item.setIcon(null); } if (!isFavorite) { - item = popup.getMenu().getItem(1); + item = popup.getMenu().findItem(R.id.ic_action_select_fav); item.setIcon(null); } if (!isLowPrior) { - item = popup.getMenu().getItem(2); + item = popup.getMenu().findItem(R.id.ic_action_select_deprioritize); item.setIcon(null); } - if (session.getDirectChatRoomIdsList().indexOf(room.getRoomId()) < 0) { - item = popup.getMenu().getItem(3); + if (!session.getDirectChatRoomIdsList().contains(room.getRoomId())) { + item = popup.getMenu().findItem(R.id.ic_action_select_direct_chat); item.setIcon(null); } + RoomMember member = room.getMember(session.getMyUserId()); + final boolean isBannedKickedRoom = (null != member) && member.kickedOrBanned(); + + if (isBannedKickedRoom) { + item = popup.getMenu().findItem(R.id.ic_action_select_remove); + + if (null != item) { + item.setTitle(R.string.forget_room); + } + } if (moreActionListener != null) { popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(final MenuItem item) { switch (item.getItemId()) { - case R.id.ic_action_select_notifications: { - moreActionListener.onToggleRoomNotifications(session, room.getRoomId()); + case R.id.ic_action_notifications_noisy: + moreActionListener.onUpdateRoomNotificationsState(session, room.getRoomId(), BingRulesManager.RoomNotificationState.ALL_MESSAGES_NOISY); break; - } + + case R.id.ic_action_notifications_all_message: + moreActionListener.onUpdateRoomNotificationsState(session, room.getRoomId(), BingRulesManager.RoomNotificationState.ALL_MESSAGES); + break; + + case R.id.ic_action_notifications_mention_only: + moreActionListener.onUpdateRoomNotificationsState(session, room.getRoomId(), BingRulesManager.RoomNotificationState.MENTIONS_ONLY); + break; + + case R.id.ic_action_notifications_mute: + moreActionListener.onUpdateRoomNotificationsState(session, room.getRoomId(), BingRulesManager.RoomNotificationState.MUTE); + break; + case R.id.ic_action_select_fav: { if (isFavorite) { moreActionListener.moveToConversations(session, room.getRoomId()); @@ -530,13 +598,21 @@ public boolean onMenuItemClick(final MenuItem item) { break; } case R.id.ic_action_select_remove: { - moreActionListener.onLeaveRoom(session, room.getRoomId()); + if (isBannedKickedRoom) { + moreActionListener.onForgetRoom(session, room.getRoomId()); + } else { + moreActionListener.onLeaveRoom(session, room.getRoomId()); + } break; } case R.id.ic_action_select_direct_chat: { moreActionListener.onToggleDirectChat(session, room.getRoomId()); break; } + case R.id.ic_action_add_homescreen_shortcut: { + moreActionListener.addHomeScreenShortcut(session, room.getRoomId()); + break; + } } return false; } @@ -591,6 +667,75 @@ public void onClick(DialogInterface dialog, int which) { .show(); } + /** + * Add a room shortcut to the home screen (Android >= O). + * + * @param context the context + * @param session the session + * @param roomId the room Id + */ + @SuppressLint("NewApi") + public static void addHomeScreenShortcut(final Context context, final MXSession session, final String roomId) { + // android >= O only + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + ShortcutManager manager = context.getSystemService(ShortcutManager.class); + + if (!manager.isRequestPinShortcutSupported()) { + return; + } + + Room room = session.getDataHandler().getRoom(roomId); + + if (null == room) { + return; + } + + String roomName = VectorUtils.getRoomDisplayName(context, session, room); + + Bitmap bitmap = null; + + // try to retrieve the avatar from the medias cache + if (!TextUtils.isEmpty(room.getAvatarUrl())) { + int size = context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size); + + // check if the thumbnail is already downloaded + File f = session.getMediasCache().thumbnailCacheFile(room.getAvatarUrl(), size); + + if (null != f) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + try { + bitmap = BitmapFactory.decodeFile(f.getPath(), options); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "decodeFile failed with an oom"); + } + } + } + + if (null == bitmap) { + bitmap = VectorUtils.getAvatar(context, VectorUtils.getAvatarColor(roomId), roomName, true); + } + + Icon icon = Icon.createWithBitmap(bitmap); + + Intent intent = new Intent(context, VectorRoomActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId); + + ShortcutInfo info = new ShortcutInfo.Builder(context, roomId) + .setShortLabel(roomName) + .setIcon(icon) + .setIntent(intent) + .build(); + + + manager.requestPinShortcut(info, null); + } + /** * Update a room Tag * @@ -643,18 +788,6 @@ public static void toggleDirectChat(final MXSession session, String roomId, fina } } - /** - * Enable or disable notifications for the given room - * - * @param session - * @param roomId - * @param listener - */ - public static void toggleNotifications(final MXSession session, final String roomId, final BingRulesManager.onBingRuleUpdateListener listener) { - BingRulesManager bingRulesManager = session.getDataHandler().getBingRulesManager(); - bingRulesManager.muteRoomNotifications(roomId, !bingRulesManager.isRoomNotificationsDisabled(roomId), listener); - } - /** * Get whether the room of the given is a direct chat * diff --git a/vector/src/main/java/im/vector/util/SlashComandsParser.java b/vector/src/main/java/im/vector/util/SlashComandsParser.java index 14487c6cf3..18a9d5e138 100755 --- a/vector/src/main/java/im/vector/util/SlashComandsParser.java +++ b/vector/src/main/java/im/vector/util/SlashComandsParser.java @@ -17,7 +17,6 @@ package im.vector.util; import android.app.AlertDialog; -import android.content.DialogInterface; import android.text.TextUtils; import org.matrix.androidsdk.util.Log; diff --git a/vector/src/main/java/im/vector/util/SlidableMediaInfo.java b/vector/src/main/java/im/vector/util/SlidableMediaInfo.java index 7a9343c363..799ea5adfd 100755 --- a/vector/src/main/java/im/vector/util/SlidableMediaInfo.java +++ b/vector/src/main/java/im/vector/util/SlidableMediaInfo.java @@ -15,7 +15,7 @@ */ package im.vector.util; -import org.matrix.androidsdk.rest.model.EncryptedFileInfo; +import org.matrix.androidsdk.rest.model.crypto.EncryptedFileInfo; import java.io.Serializable; diff --git a/vector/src/main/java/im/vector/util/ThemeUtils.java b/vector/src/main/java/im/vector/util/ThemeUtils.java index 657a851c31..2053bbea8d 100644 --- a/vector/src/main/java/im/vector/util/ThemeUtils.java +++ b/vector/src/main/java/im/vector/util/ThemeUtils.java @@ -23,6 +23,7 @@ import android.preference.PreferenceManager; import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; +import android.support.design.widget.TabLayout; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.TypedValue; @@ -47,6 +48,7 @@ import im.vector.activity.SplashActivity; import im.vector.activity.VectorBaseSearchActivity; import im.vector.activity.VectorCallViewActivity; +import im.vector.activity.VectorGroupDetailsActivity; import im.vector.activity.VectorHomeActivity; import im.vector.activity.VectorMediasPickerActivity; import im.vector.activity.VectorMediasViewerActivity; @@ -178,6 +180,8 @@ public static void setActivityTheme(Activity activity) { activity.setTheme(R.style.AppTheme_Dark); } else if (activity instanceof LockScreenActivity) { activity.setTheme(R.style.Vector_Lock_Dark); + } else if (activity instanceof VectorGroupDetailsActivity) { + activity.setTheme(R.style.AppTheme_Dark); } } @@ -234,6 +238,8 @@ public static void setActivityTheme(Activity activity) { activity.setTheme(R.style.AppTheme_Black); } else if (activity instanceof LockScreenActivity) { activity.setTheme(R.style.Vector_Lock_Black); + } else if (activity instanceof VectorGroupDetailsActivity) { + activity.setTheme(R.style.AppTheme_Black); } } @@ -247,6 +253,34 @@ public static void setActivityTheme(Activity activity) { mColorByAttr.clear(); } + /** + * Set the TabLayout colors. + * It seems that there is no proper way to manage it with the manifest file. + * + * @param activity the activity + * @param layout the layout + */ + public static void setTabLayoutTheme(Activity activity, TabLayout layout) { + + if (activity instanceof VectorGroupDetailsActivity) { + int textColor; + int underlineColor; + int backgroundColor; + + if (TextUtils.equals(getApplicationTheme(activity), THEME_LIGHT_VALUE)) { + underlineColor = textColor = ContextCompat.getColor(activity, android.R.color.white); + backgroundColor = ContextCompat.getColor(activity, R.color.tab_groups); + } else { + underlineColor = textColor = ContextCompat.getColor(activity, R.color.tab_groups); + backgroundColor = getColor(activity, R.attr.primary_color); + } + + layout.setTabTextColors(textColor, textColor); + layout.setSelectedTabIndicatorColor(underlineColor); + layout.setBackgroundColor(backgroundColor); + } + } + /** * Translates color attributes to colors * diff --git a/vector/src/main/java/im/vector/util/VectorImageGetter.java b/vector/src/main/java/im/vector/util/VectorImageGetter.java new file mode 100644 index 0000000000..3c60804f77 --- /dev/null +++ b/vector/src/main/java/im/vector/util/VectorImageGetter.java @@ -0,0 +1,154 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.util; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.support.v4.content.res.ResourcesCompat; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.util.Log; + +import android.text.Html; + +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import im.vector.R; +import im.vector.VectorApp; + +public class VectorImageGetter implements Html.ImageGetter { + private final String LOG_TAG = VectorImageGetter.class.getSimpleName(); + + // application image placeholder + private static Drawable mPlaceHolder = null; + + // source to image map + private Map mBitmapCache = new HashMap<>(); + + // pending source downloads + private Set mPendingDownloads = new HashSet<>(); + + /** + * Image download listener + */ + public interface OnImageDownloadListener { + /** + * An image has been downloaded. + * + * @param source the image URL + */ + void onImageDownloaded(String source); + } + + // + private MXSession mSession; + + // listener + private OnImageDownloadListener mListener; + + /** + * Constructor + * + * @param session the session + */ + public VectorImageGetter(MXSession session) { + mSession = session; + } + + /** + * Set the listener + * + * @param listener the listener + */ + public void setListener(OnImageDownloadListener listener) { + mListener = listener; + } + + @Override + public Drawable getDrawable(String source) { + if (mBitmapCache.containsKey(source)) { + Log.d(LOG_TAG, "## getDrawable() : " + source + " already cached"); + return mBitmapCache.get(source); + } + + if (!mPendingDownloads.contains(source)) { + Log.d(LOG_TAG, "## getDrawable() : starts a task to download " + source); + try { + new ImageDownloaderTask().execute(source); + mPendingDownloads.add(source); + } catch (Throwable t) { + Log.e(LOG_TAG, "## getDrawable() failed " + t.getMessage()); + } + } else { + Log.d(LOG_TAG, "## getDrawable() : " + source + " is downloading"); + } + + if (null == mPlaceHolder) { + mPlaceHolder = ResourcesCompat.getDrawable(VectorApp.getInstance().getResources(), R.drawable.filetype_image, null); + mPlaceHolder.setBounds(0, 0, mPlaceHolder.getIntrinsicWidth(), mPlaceHolder.getIntrinsicHeight()); + } + + return mPlaceHolder; + } + + + private class ImageDownloaderTask extends AsyncTask { + private String mSource; + + @Override + protected Bitmap doInBackground(Object... params) { + mSource = (String) params[0]; + Log.d(LOG_TAG, "## doInBackground() : " + mSource); + try { + return BitmapFactory.decodeStream(new URL(mSession.getContentManager().getDownloadableUrl(mSource)).openConnection().getInputStream()); + } catch (Throwable t) { + Log.e(LOG_TAG, "## ImageDownloader() failed " + t.getMessage()); + } + + return null; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + Log.d(LOG_TAG, "## doInBackground() : bitmap " + bitmap); + + mPendingDownloads.remove(mSource); + + if (null != bitmap) { + Drawable drawable = new BitmapDrawable(VectorApp.getInstance().getResources(), bitmap); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + + mBitmapCache.put(mSource, drawable); + + try { + if (null != mListener) { + mListener.onImageDownloaded(mSource); + } + } catch (Throwable t) { + Log.e(LOG_TAG, "## ImageDownloader() failed " + t.getMessage()); + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/util/VectorMarkdownParser.java b/vector/src/main/java/im/vector/util/VectorMarkdownParser.java index d6229bad6e..419226c543 100755 --- a/vector/src/main/java/im/vector/util/VectorMarkdownParser.java +++ b/vector/src/main/java/im/vector/util/VectorMarkdownParser.java @@ -28,6 +28,9 @@ import android.webkit.JavascriptInterface; import android.webkit.WebView; +import java.util.Timer; +import java.util.TimerTask; + /** * Markdown parser. * This class uses a webview. @@ -113,6 +116,9 @@ public void markdownToHtml(final String markdownText, final IVectorMarkdownParse mMarkDownWebAppInterface.initParams(markdownText, listener); try { + // the conversion starts + mMarkDownWebAppInterface.start(); + // call the javascript method if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { loadUrl(String.format("javascript:convertToHtml('%s')", escapeText(markdownText))); @@ -120,6 +126,7 @@ public void markdownToHtml(final String markdownText, final IVectorMarkdownParse evaluateJavascript(String.format("convertToHtml('%s')", escapeText(markdownText)), null); } } catch (Exception e) { + mMarkDownWebAppInterface.cancel(); Log.e(LOG_TAG, "## markdownToHtml() : failed " + e.getMessage()); listener.onMarkdownParsed(markdownText, text); } @@ -151,6 +158,11 @@ private class MarkDownWebAppInterface { */ private IVectorMarkdownParserListener mListener; + /** + * Defines watchdog timer + */ + private Timer mWatchdogTimer; + /** * Init the search params. * @@ -162,6 +174,54 @@ public void initParams(String textToParse, IVectorMarkdownParserListener listene mListener = listener; } + /** + * The parsing starts. + */ + public void start() { + Log.d(LOG_TAG, "## start() : Markdown starts"); + + try { + // monitor the parsing as there is no way to detect if there was an error in the JS. + mWatchdogTimer = new Timer(); + mWatchdogTimer.schedule(new TimerTask() { + @Override + public void run() { + if (null != mListener) { + Log.d(LOG_TAG, "## start() : delay expires"); + + try { + mListener.onMarkdownParsed(mTextToParse, mTextToParse); + } catch (Exception e) { + Log.e(LOG_TAG, "## wOnParse() " + e.getMessage()); + } + } + done(); + } + }, 300); + } catch (Throwable e) { + Log.e(LOG_TAG, "## start() : failed to starts " + e.getMessage()); + } + } + + /** + * Cancel the markdown parser + */ + public void cancel() { + Log.e(LOG_TAG, "## cancel()"); + done(); + } + + /** + * The parsing is done + */ + private void done() { + if (null != mWatchdogTimer) { + mWatchdogTimer.cancel(); + mWatchdogTimer = null; + } + mListener = null; + } + @JavascriptInterface public void wOnParse(String HTMLText) { if (!TextUtils.isEmpty(HTMLText)) { @@ -179,11 +239,17 @@ public void wOnParse(String HTMLText) { } if (null != mListener) { + Log.d(LOG_TAG, "## wOnParse() : parse done"); + try { mListener.onMarkdownParsed(mTextToParse, HTMLText); } catch (Exception e) { Log.e(LOG_TAG, "## wOnParse() " + e.getMessage()); } + + done(); + } else { + Log.d(LOG_TAG, "## wOnParse() : parse required too much time"); } } } diff --git a/vector/src/main/java/im/vector/util/VectorRoomMediasSender.java b/vector/src/main/java/im/vector/util/VectorRoomMediasSender.java index 4494cd14e3..f83032f5c9 100755 --- a/vector/src/main/java/im/vector/util/VectorRoomMediasSender.java +++ b/vector/src/main/java/im/vector/util/VectorRoomMediasSender.java @@ -23,7 +23,9 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.support.v4.app.FragmentManager; import android.text.Html; import android.text.TextUtils; @@ -35,7 +37,7 @@ import android.widget.Toast; import org.matrix.androidsdk.db.MXMediasCache; -import org.matrix.androidsdk.rest.model.Message; +import org.matrix.androidsdk.rest.model.message.Message; import org.matrix.androidsdk.util.ImageUtils; import org.matrix.androidsdk.util.ResourceUtils; @@ -260,29 +262,42 @@ public void run() { * @param sharedDataItem the media item. */ private void sendTextMessage(RoomMediaMessage sharedDataItem) { - CharSequence sequence = sharedDataItem.getText(); + final CharSequence sequence = sharedDataItem.getText(); String htmlText = sharedDataItem.getHtmlText(); - String text = null; - if (null == sequence) { - if (null != htmlText) { - text = Html.fromHtml(htmlText).toString(); - } + // content only text -> insert it in the room editor + // to let the user decides to send the message + if (!TextUtils.isEmpty(sequence) && (null == htmlText)) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + mVectorRoomActivity.insertTextInTextEditor(sequence.toString()); + } + }); } else { - text = sequence.toString(); - } - Log.d(LOG_TAG, "sendTextMessage " + text); + String text = null; - final String fText = text; - final String fHtmlText = htmlText; - - mVectorRoomActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - mVectorRoomActivity.sendMessage(fText, fHtmlText, Message.FORMAT_MATRIX_HTML); + if (null == sequence) { + if (null != htmlText) { + text = Html.fromHtml(htmlText).toString(); + } + } else { + text = sequence.toString(); } - }); + + Log.d(LOG_TAG, "sendTextMessage " + text); + + final String fText = text; + final String fHtmlText = htmlText; + + mVectorRoomActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mVectorRoomActivity.sendMessage(fText, fHtmlText, Message.FORMAT_MATRIX_HTML); + } + }); + } // manage others if (mSharedDataItems.size() > 0) { diff --git a/vector/src/main/java/im/vector/util/VectorUtils.java b/vector/src/main/java/im/vector/util/VectorUtils.java index 634d60010a..1586771334 100755 --- a/vector/src/main/java/im/vector/util/VectorUtils.java +++ b/vector/src/main/java/im/vector/util/VectorUtils.java @@ -55,7 +55,9 @@ import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.MatrixError; -import org.matrix.androidsdk.rest.model.PublicRoom; +import org.matrix.androidsdk.rest.model.group.Group; +import org.matrix.androidsdk.rest.model.group.GroupProfile; +import org.matrix.androidsdk.rest.model.publicroom.PublicRoom; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.util.ImageUtils; @@ -71,7 +73,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; import java.util.regex.Pattern; import im.vector.R; @@ -250,9 +251,9 @@ public int compare(RoomMember m1, RoomMember m2) { if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) { - if (!TextUtils.isEmpty(member.getInviterId())) { + if (!TextUtils.isEmpty(member.mSender)) { // extract who invited us to the room - displayName = context.getString(R.string.room_displayname_invite_from, roomState.getMemberName(member.getInviterId())); + displayName = context.getString(R.string.room_displayname_invite_from, roomState.getMemberName(member.mSender)); } else { displayName = context.getString(R.string.room_displayname_room_invite); } @@ -376,7 +377,7 @@ private static String getInitialLetter(String name) { int idx = 0; char initial = name.charAt(idx); - if ((initial == '@' || initial == '#') && (name.length() > 1)) { + if ((initial == '@' || initial == '#' || initial == '+') && (name.length() > 1)) { idx++; } @@ -402,7 +403,7 @@ private static String getInitialLetter(String name) { firstChar = name.substring(idx, idx + chars); } - return firstChar.toUpperCase(); + return firstChar.toUpperCase(VectorApp.getApplicationLocale()); } /** @@ -472,6 +473,20 @@ public static void loadRoomAvatar(Context context, MXSession session, ImageView } } + /** + * Set the group avatar in an imageView. + * + * @param context the context + * @param session the session + * @param imageView the image view + * @param group the group + */ + public static void loadGroupAvatar(Context context, MXSession session, ImageView imageView, Group group) { + if (null != group) { + VectorUtils.loadUserAvatar(context, session, imageView, group.getAvatarUrl(), group.getGroupId(), group.getDisplayName()); + } + } + /** * Set the call avatar in an imageView. * @@ -590,18 +605,20 @@ public static void loadUserAvatar(final Context context, final MXSession session if (null != bitmap) { imageView.setImageBitmap(bitmap); - final String tag = avatarUrl + userId + displayName; - imageView.setTag(tag); + if (!TextUtils.isEmpty(avatarUrl)) { + final String tag = avatarUrl + userId + displayName; + imageView.setTag(tag); - if (!MXMediasCache.isMediaUrlUnreachable(avatarUrl)) { - mImagesThreadHandler.post(new Runnable() { - @Override - public void run() { - if (TextUtils.equals(tag, (String) imageView.getTag())) { - session.getMediasCache().loadAvatarThumbnail(session.getHomeServerConfig(), imageView, avatarUrl, context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size), bitmap); + if (!MXMediasCache.isMediaUrlUnreachable(avatarUrl)) { + mImagesThreadHandler.post(new Runnable() { + @Override + public void run() { + if (TextUtils.equals(tag, (String) imageView.getTag())) { + session.getMediasCache().loadAvatarThumbnail(session.getHomeServerConfig(), imageView, avatarUrl, context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size), bitmap); + } } - } - }); + }); + } } } else { final String tmpTag0 = "00" + avatarUrl + "-" + userId + "--" + displayName; @@ -615,7 +632,7 @@ public void run() { imageView.setTag(null); setDefaultMemberAvatar(imageView, userId, displayName); - if (!MXMediasCache.isMediaUrlUnreachable(avatarUrl)) { + if (!TextUtils.isEmpty(avatarUrl) && !MXMediasCache.isMediaUrlUnreachable(avatarUrl)) { final String tmpTag1 = "11" + avatarUrl + "-" + userId + "--" + displayName; imageView.setTag(tmpTag1); diff --git a/vector/src/main/java/im/vector/view/CodeBlockNestedScrollView.java b/vector/src/main/java/im/vector/view/CodeBlockNestedScrollView.java new file mode 100755 index 0000000000..f253d50212 --- /dev/null +++ b/vector/src/main/java/im/vector/view/CodeBlockNestedScrollView.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.view; + +import android.content.Context; +import android.support.v4.widget.NestedScrollView; +import android.util.AttributeSet; + +public class CodeBlockNestedScrollView extends NestedScrollView { + + public CodeBlockNestedScrollView(Context context) { + super(context); + } + + public CodeBlockNestedScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CodeBlockNestedScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(500, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/view/HomeSectionView.java b/vector/src/main/java/im/vector/view/HomeSectionView.java index 855bf2e5fd..c615c74327 100644 --- a/vector/src/main/java/im/vector/view/HomeSectionView.java +++ b/vector/src/main/java/im/vector/view/HomeSectionView.java @@ -131,12 +131,15 @@ private void onDataUpdated() { // reported by GA // the adapter value is tested by it seems crashed when calling getBadgeCount try { - setVisibility(mHideIfEmpty && mAdapter.isEmpty() ? GONE : VISIBLE); - final int badgeCount = mAdapter.getBadgeCount(); + boolean isEmpty = mAdapter.isEmpty(); + boolean hasNoResult = mAdapter.hasNoResult(); + int badgeCount = mAdapter.getBadgeCount(); + + setVisibility(mHideIfEmpty && isEmpty ? GONE : VISIBLE); mBadge.setText(RoomUtils.formatUnreadMessagesCounter(badgeCount)); mBadge.setVisibility(badgeCount == 0 ? GONE : VISIBLE); - mRecyclerView.setVisibility(mAdapter.hasNoResult() ? GONE : VISIBLE); - mPlaceHolder.setVisibility(mAdapter.hasNoResult() ? VISIBLE : GONE); + mRecyclerView.setVisibility(hasNoResult ? GONE : VISIBLE); + mPlaceHolder.setVisibility(hasNoResult ? VISIBLE : GONE); } catch (Exception e) { Log.e(LOG_TAG, "## onDataUpdated() failed " + e.getMessage()); } @@ -190,10 +193,10 @@ public void setHideIfEmpty(final boolean hideIfEmpty) { * @param invitationListener listener for invite buttons * @param moreActionListener listener for room menu */ - public void setupRecyclerView(final RecyclerView.LayoutManager layoutManager, @LayoutRes final int itemResId, - final boolean nestedScrollEnabled, final HomeRoomAdapter.OnSelectRoomListener onSelectRoomListener, - final AbsAdapter.InvitationListener invitationListener, - final AbsAdapter.MoreRoomActionListener moreActionListener) { + public void setupRoomRecyclerView(final RecyclerView.LayoutManager layoutManager, @LayoutRes final int itemResId, + final boolean nestedScrollEnabled, final HomeRoomAdapter.OnSelectRoomListener onSelectRoomListener, + final AbsAdapter.RoomInvitationListener invitationListener, + final AbsAdapter.MoreRoomActionListener moreActionListener) { mRecyclerView.setLayoutManager(layoutManager); mRecyclerView.setHasFixedSize(true); mRecyclerView.setNestedScrollingEnabled(nestedScrollEnabled); diff --git a/vector/src/main/java/im/vector/view/PillImageView.java b/vector/src/main/java/im/vector/view/PillImageView.java new file mode 100644 index 0000000000..6a45720e3d --- /dev/null +++ b/vector/src/main/java/im/vector/view/PillImageView.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.view; + +import android.content.Context; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.util.AttributeSet; + +import java.lang.ref.WeakReference; + +/** + * Avatar image view used in PillView + */ +public class PillImageView extends VectorCircularImageView { + // listener + private WeakReference mOnUpdateListener = null; + + public PillImageView(Context context) { + super(context); + } + + public PillImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PillImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void setCircularImageDrawable(final RoundedBitmapDrawable cachedDrawable) { + super.setCircularImageDrawable(cachedDrawable); + + if ((null != mOnUpdateListener) && (null != mOnUpdateListener.get())) { + mOnUpdateListener.get().onAvatarUpdate(); + } + } + + /** + * Update the update listener + * + * @param listener the new update listener + */ + public void setOnUpdateListener(PillView.OnUpdateListener listener) { + mOnUpdateListener = new WeakReference<>(listener); + } +} diff --git a/vector/src/main/java/im/vector/view/PillView.java b/vector/src/main/java/im/vector/view/PillView.java index d0418ad675..74f3854acb 100644 --- a/vector/src/main/java/im/vector/view/PillView.java +++ b/vector/src/main/java/im/vector/view/PillView.java @@ -22,24 +22,40 @@ import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.callback.ApiCallback; +import org.matrix.androidsdk.rest.model.MatrixError; +import org.matrix.androidsdk.rest.model.User; +import org.matrix.androidsdk.util.Log; + +import java.lang.ref.WeakReference; -import im.vector.Matrix; import im.vector.R; +import im.vector.VectorApp; +import im.vector.util.VectorUtils; + /** * */ public class PillView extends LinearLayout { private static final String LOG_TAG = PillView.class.getSimpleName(); + // pill item views private TextView mTextView; + private PillImageView mAvatarView; private View mPillLayout; + // update listener + public interface OnUpdateListener { + void onAvatarUpdate(); + } + + private WeakReference mOnUpdateListener = null; + /** * constructors **/ @@ -64,33 +80,47 @@ public PillView(Context context, AttributeSet attrs, int defStyle) { private void initView() { View.inflate(getContext(), R.layout.pill_view, this); mTextView = findViewById(R.id.pill_text_view); + mAvatarView = findViewById(R.id.pill_avatar_view); mPillLayout = findViewById(R.id.pill_layout); } /** - * Tells if a pill can be displayed for this url. + * Extract the linked URL from the universal link * - * @param url the url - * @return true if a pill can be made. + * @param url the universal link + * @return the url */ - public static boolean isPillable(String url) { + private static String getLinkedUrl(String url) { boolean isSupported = (null != url) && url.startsWith("https://matrix.to/#/"); if (isSupported) { - String linkedItem = url.substring("https://matrix.to/#/".length()); - isSupported = MXSession.isRoomAlias(linkedItem) || MXSession.isUserId(linkedItem); + return url.substring("https://matrix.to/#/".length()); } - return isSupported; + return null; + } + + /** + * Tells if a pill can be displayed for this url. + * + * @param url the url + * @return true if a pill can be made. + */ + public static boolean isPillable(String url) { + String linkedUrl = getLinkedUrl(url); + + return (null != linkedUrl) && (MXSession.isRoomAlias(linkedUrl) || MXSession.isUserId(linkedUrl)); } /** - * Update the pills text. + * Update the pills data * * @param text the pills * @param url the URL */ - public void setText(CharSequence text, String url) { + public void initData(final CharSequence text, final String url, final MXSession session, OnUpdateListener listener) { + mOnUpdateListener = new WeakReference<>(listener); + mAvatarView.setOnUpdateListener(listener); mTextView.setText(text.toString()); TypedArray a = getContext().getTheme().obtainStyledAttributes(new int[]{MXSession.isRoomAlias(text.toString()) ? R.attr.pill_background_room_alias : R.attr.pill_background_user_id}); @@ -103,6 +133,43 @@ public void setText(CharSequence text, String url) { attributeResourceId = a.getResourceId(0, 0); a.recycle(); mTextView.setTextColor(ContextCompat.getColor(getContext(), attributeResourceId)); + + String linkedUrl = getLinkedUrl(url); + + if (MXSession.isUserId(linkedUrl)) { + User user = session.getDataHandler().getUser(linkedUrl); + + if (null == user) { + user = new User(); + user.user_id = linkedUrl; + } + + VectorUtils.loadUserAvatar(VectorApp.getInstance(), session, mAvatarView, user); + } else { + session.getDataHandler().roomIdByAlias(linkedUrl, new ApiCallback() { + @Override + public void onSuccess(String roomId) { + if (null != mOnUpdateListener) { + VectorUtils.loadRoomAvatar(VectorApp.getInstance(), session, mAvatarView, session.getDataHandler().getRoom(roomId)); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## initData() : roomIdByAlias failed " + e.getMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## initData() : roomIdByAlias failed " + e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## initData() : roomIdByAlias failed " + e.getMessage()); + } + }); + } } /** diff --git a/vector/src/main/java/im/vector/view/RiotViewPager.java b/vector/src/main/java/im/vector/view/RiotViewPager.java index 3d0748b936..56280a515f 100644 --- a/vector/src/main/java/im/vector/view/RiotViewPager.java +++ b/vector/src/main/java/im/vector/view/RiotViewPager.java @@ -18,9 +18,10 @@ import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; -import android.util.Log; import android.view.MotionEvent; +import org.matrix.androidsdk.util.Log; + /** * Patch the issue "https://code.google.com/p/android/issues/detail?id=66620" */ diff --git a/vector/src/main/java/im/vector/view/UrlPreviewView.java b/vector/src/main/java/im/vector/view/UrlPreviewView.java new file mode 100644 index 0000000000..d673f39354 --- /dev/null +++ b/vector/src/main/java/im/vector/view/UrlPreviewView.java @@ -0,0 +1,151 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.view; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.matrix.androidsdk.MXSession; +import org.matrix.androidsdk.rest.model.URLPreview; +import org.matrix.androidsdk.util.Log; + +import java.util.HashSet; + +import im.vector.R; +import im.vector.VectorApp; +import im.vector.util.PreferencesManager; + +/** + * + */ +public class UrlPreviewView extends LinearLayout { + private static final String LOG_TAG = UrlPreviewView.class.getSimpleName(); + + // private item views + private ImageView mImageView; + private TextView mTitleTextView; + private TextView mDescriptionTextView; + private View mCloseView; + + // dismissed when clicking on mCloseView + private boolean mIsDismissed = false; + + private String mUID = null; + + // save + private static HashSet mDismissedUrlsPreviews = null; + + private static final String DISMISSED_URL_PREVIEWS_PREF_KEY = "DISMISSED_URL_PREVIEWS_PREF_KEY"; + + /** + * constructors + **/ + public UrlPreviewView(Context context) { + super(context); + initView(); + } + + public UrlPreviewView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + public UrlPreviewView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initView(); + } + + /** + * Common initialisation method. + */ + private void initView() { + View.inflate(getContext(), R.layout.url_preview_view, this); + mImageView = findViewById(R.id.url_preview_image_view); + mTitleTextView = findViewById(R.id.url_preview_title_text_view); + mDescriptionTextView = findViewById(R.id.url_preview_description_text_view); + mCloseView = findViewById(R.id.url_preview_hide_image_view); + + mCloseView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mIsDismissed = true; + UrlPreviewView.this.setVisibility(View.GONE); + + mDismissedUrlsPreviews.add(mUID); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(VectorApp.getInstance()); + SharedPreferences.Editor editor = preferences.edit(); + + editor.putStringSet(DISMISSED_URL_PREVIEWS_PREF_KEY, mDismissedUrlsPreviews); + editor.commit(); + } + }); + } + + /** + * Tells if the URL preview defines by uid has been dismissed. + * + * @param uid the url preview id + * @return true if it has been dismissed + */ + public static boolean didUrlPreviewDismiss(String uid) { + if (null == mDismissedUrlsPreviews) { + mDismissedUrlsPreviews = new HashSet<>(PreferenceManager.getDefaultSharedPreferences(VectorApp.getInstance()).getStringSet(DISMISSED_URL_PREVIEWS_PREF_KEY, new HashSet())); + } + + return mDismissedUrlsPreviews.contains(uid); + } + + /** + * Set the URL preview. + * + * @param context the context + * @param session the session + * @param preview the url preview + * @param uid unique identifier of this preview + */ + public void setUrlPreview(Context context, MXSession session, URLPreview preview, String uid) { + Log.d(LOG_TAG, "## setUrlPreview " + this); + + if ((null == preview) || mIsDismissed || didUrlPreviewDismiss(uid) || !session.isURLPreviewEnabled()) { + setVisibility(View.GONE); + } else { + setVisibility(View.VISIBLE); + session.getMediasCache().loadAvatarThumbnail(session.getHomeServerConfig(), mImageView, preview.getThumbnailURL(), context.getResources().getDimensionPixelSize(R.dimen.profile_avatar_size)); + + if ((null != preview.getRequestedURL()) && (null != preview.getTitle())) { + mTitleTextView.setText(Html.fromHtml("" + preview.getTitle() + "")); + } else if (null != preview.getTitle()) { + mTitleTextView.setText(preview.getTitle()); + } else { + mTitleTextView.setText(preview.getRequestedURL()); + } + mTitleTextView.setMovementMethod(LinkMovementMethod.getInstance()); + + mDescriptionTextView.setText(preview.getDescription()); + + mUID = uid; + } + } +} diff --git a/vector/src/main/java/im/vector/view/VectorCircularImageView.java b/vector/src/main/java/im/vector/view/VectorCircularImageView.java index bab1adfd3f..61115c89ea 100755 --- a/vector/src/main/java/im/vector/view/VectorCircularImageView.java +++ b/vector/src/main/java/im/vector/view/VectorCircularImageView.java @@ -90,6 +90,15 @@ protected int sizeOf(String key, RoundedBitmapDrawable drawable) { private static Map>> mPendingConversion = new HashMap<>(); + /** + * Update the image drawable with the rounded bitmap. + * + * @param cachedDrawable the bitmap drawable. + */ + protected void setCircularImageDrawable(final RoundedBitmapDrawable cachedDrawable) { + super.setImageDrawable(cachedDrawable); + } + /** * Update the bitmap. * The bitmap is first squared before adding corners @@ -108,7 +117,7 @@ public void setImageBitmap(final Bitmap bm) { // Create a RoundedBitmapDrawable might be slow RoundedBitmapDrawable cachedDrawable = mCache.get(key); if (null != cachedDrawable) { - super.setImageDrawable(cachedDrawable); + setCircularImageDrawable(cachedDrawable); return; } @@ -182,7 +191,7 @@ public void run() { for (Pair pair : pairs) { // update only if the tag is the same if (pair.second.getTag() == pair.first) { - pair.second.setImageDrawable(drawable); + pair.second.setCircularImageDrawable(drawable); } } } diff --git a/vector/src/main/java/im/vector/widgets/WidgetContent.java b/vector/src/main/java/im/vector/widgets/WidgetContent.java index df74f76a70..a02d07bc84 100755 --- a/vector/src/main/java/im/vector/widgets/WidgetContent.java +++ b/vector/src/main/java/im/vector/widgets/WidgetContent.java @@ -17,11 +17,11 @@ package im.vector.widgets; import android.text.TextUtils; -import android.util.Log; import com.google.gson.JsonElement; import org.matrix.androidsdk.util.JsonUtils; +import org.matrix.androidsdk.util.Log; import java.io.Serializable; import java.util.Map; diff --git a/vector/src/main/java/im/vector/widgets/WidgetsManager.java b/vector/src/main/java/im/vector/widgets/WidgetsManager.java index 9aed217a77..4558373ee6 100755 --- a/vector/src/main/java/im/vector/widgets/WidgetsManager.java +++ b/vector/src/main/java/im/vector/widgets/WidgetsManager.java @@ -333,7 +333,7 @@ public void createJitsiWidget(MXSession session, Room room, boolean withVideo, f widgetSessionId = widgetSessionId.substring(0, 7); } String roomId = room.getRoomId(); - String confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(); + String confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorApp.getApplicationLocale()); // TODO: This url may come from scalar API // Note: this url can be used as is inside a web container (like iframe for Riot-web) diff --git a/vector/src/main/res/drawable-hdpi/ic_people_black_24dp.png b/vector/src/main/res/drawable-hdpi/ic_people_black_24dp.png deleted file mode 100644 index 078216628d..0000000000 Binary files a/vector/src/main/res/drawable-hdpi/ic_people_black_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-mdpi/ic_people_black_24dp.png b/vector/src/main/res/drawable-mdpi/ic_people_black_24dp.png deleted file mode 100644 index e2600a4d93..0000000000 Binary files a/vector/src/main/res/drawable-mdpi/ic_people_black_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_people_black_24dp.png b/vector/src/main/res/drawable-xhdpi/ic_people_black_24dp.png deleted file mode 100644 index c2e9ffea1d..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/ic_people_black_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_people_black_24dp.png b/vector/src/main/res/drawable-xxhdpi/ic_people_black_24dp.png deleted file mode 100644 index 5a8b5d05e2..0000000000 Binary files a/vector/src/main/res/drawable-xxhdpi/ic_people_black_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_power_settings.png b/vector/src/main/res/drawable-xxhdpi/ic_power_settings.png new file mode 100644 index 0000000000..d206fa435a Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_power_settings.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/riot_tab_groups.png b/vector/src/main/res/drawable-xxhdpi/riot_tab_groups.png new file mode 100644 index 0000000000..7dc4232ae3 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/riot_tab_groups.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/riot_tab_rooms.png b/vector/src/main/res/drawable-xxhdpi/riot_tab_rooms.png new file mode 100644 index 0000000000..4f3b8cfb41 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/riot_tab_rooms.png differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_people_black_24dp.png b/vector/src/main/res/drawable-xxxhdpi/ic_people_black_24dp.png deleted file mode 100644 index 2994e7caa6..0000000000 Binary files a/vector/src/main/res/drawable-xxxhdpi/ic_people_black_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable/vector_tabbar_background_group_light.xml b/vector/src/main/res/drawable/vector_tabbar_background_group_light.xml new file mode 100644 index 0000000000..ba8904ad99 --- /dev/null +++ b/vector/src/main/res/drawable/vector_tabbar_background_group_light.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml b/vector/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml new file mode 100644 index 0000000000..8db718af96 --- /dev/null +++ b/vector/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml b/vector/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml new file mode 100644 index 0000000000..4f594ea72c --- /dev/null +++ b/vector/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_lock_screen.xml b/vector/src/main/res/layout/activity_lock_screen.xml index 164afdcf7b..36b81490ce 100644 --- a/vector/src/main/res/layout/activity_lock_screen.xml +++ b/vector/src/main/res/layout/activity_lock_screen.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_vector_registration_captcha.xml b/vector/src/main/res/layout/activity_vector_registration_captcha.xml index c8b6c97ce5..3e665f1e1e 100644 --- a/vector/src/main/res/layout/activity_vector_registration_captcha.xml +++ b/vector/src/main/res/layout/activity_vector_registration_captcha.xml @@ -2,41 +2,58 @@ + android:layout_height="match_parent"> + android:layout_height="wrap_content" + android:layout_marginBottom="27dp" + android:layout_marginTop="30dp"> + - + android:src="@drawable/logo_login">
+ android:text="@string/auth_recaptcha_message" + android:textSize="16sp" /> - + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/adapter_item_circular_room_view.xml b/vector/src/main/res/layout/adapter_item_circular_room_view.xml old mode 100644 new mode 100755 diff --git a/vector/src/main/res/layout/adapter_item_group_invite.xml b/vector/src/main/res/layout/adapter_item_group_invite.xml new file mode 100644 index 0000000000..ac39a98548 --- /dev/null +++ b/vector/src/main/res/layout/adapter_item_group_invite.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + +