Java tutorial
/* * Copyright 2015 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.neon.activity; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.DownloadManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Parcelable; import android.preference.PreferenceManager; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; import org.matrix.androidsdk.MXDataHandler; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.call.IMXCall; import org.matrix.androidsdk.crypto.data.MXDeviceInfo; import org.matrix.androidsdk.crypto.data.MXUsersDevicesMap; import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.data.RoomPreviewData; import org.matrix.androidsdk.data.RoomSummary; import org.matrix.androidsdk.data.store.IMXStore; import org.matrix.androidsdk.db.MXMediasCache; 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.PowerLevels; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.util.Log; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import im.neon.Matrix; import im.neon.MyPresenceManager; import im.neon.R; import im.neon.VectorApp; import im.neon.adapters.VectorRoomsSelectionAdapter; import im.neon.contacts.ContactsManager; import im.neon.contacts.PIDsRetriever; import im.neon.fragments.AccountsSelectionDialogFragment; import im.neon.fragments.VectorUnknownDevicesFragment; import im.neon.ga.GAHelper; import im.neon.gcm.GcmRegistrationManager; import im.neon.services.EventStreamService; import im.neon.util.VectorUtils; import me.leolin.shortcutbadger.ShortcutBadger; /** * Contains useful functions which are called in multiple activities. */ public class CommonActivityUtils { private static final String LOG_TAG = "CommonActivityUtils"; /** * Mime types **/ public static final String MIME_TYPE_JPEG = "image/jpeg"; public static final String MIME_TYPE_JPG = "image/jpg"; public static final String MIME_TYPE_IMAGE_ALL = "image/*"; public static final String MIME_TYPE_ALL_CONTENT = "*/*"; /** * Schemes */ private static final String HTTP_SCHEME = "http://"; private static final String HTTPS_SCHEME = "https://"; // global helper constants: /** * The view is visible **/ public static final float UTILS_OPACITY_NONE = 1f; /** * The view is half dimmed **/ public static final float UTILS_OPACITY_HALF = 0.5f; /** * The view is hidden **/ public static final float UTILS_OPACITY_FULL = 0f; public static final boolean UTILS_DISPLAY_PROGRESS_BAR = true; public static final boolean UTILS_HIDE_PROGRESS_BAR = false; // room details members: public static final String KEY_GROUPS_EXPANDED_STATE = "KEY_GROUPS_EXPANDED_STATE"; public static final String KEY_SEARCH_PATTERN = "KEY_SEARCH_PATTERN"; public static final boolean GROUP_IS_EXPANDED = true; public static final boolean GROUP_IS_COLLAPSED = false; // power levels public static final float UTILS_POWER_LEVEL_ADMIN = 100; public static final float UTILS_POWER_LEVEL_MODERATOR = 50; private static final int ROOM_SIZE_ONE_TO_ONE = 2; // Android M permission request code management private static final boolean PERMISSIONS_GRANTED = true; private static final boolean PERMISSIONS_DENIED = !PERMISSIONS_GRANTED; public static final int PERMISSION_BYPASSED = 0x0; public static final int PERMISSION_CAMERA = 0x1; private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0x1 << 1; private static final int PERMISSION_RECORD_AUDIO = 0x1 << 2; private static final int PERMISSION_READ_CONTACTS = 0x1 << 3; public static final int REQUEST_CODE_PERMISSION_AUDIO_IP_CALL = PERMISSION_RECORD_AUDIO; public static final int REQUEST_CODE_PERMISSION_VIDEO_IP_CALL = PERMISSION_CAMERA | PERMISSION_RECORD_AUDIO; public static final int REQUEST_CODE_PERMISSION_TAKE_PHOTO = PERMISSION_CAMERA | PERMISSION_WRITE_EXTERNAL_STORAGE; public static final int REQUEST_CODE_PERMISSION_MEMBERS_SEARCH = PERMISSION_READ_CONTACTS; public static final int REQUEST_CODE_PERMISSION_MEMBER_DETAILS = PERMISSION_READ_CONTACTS; public static final int REQUEST_CODE_PERMISSION_ROOM_DETAILS = PERMISSION_CAMERA; public static final int REQUEST_CODE_PERMISSION_VIDEO_RECORDING = PERMISSION_CAMERA | PERMISSION_RECORD_AUDIO; public static final int REQUEST_CODE_PERMISSION_HOME_ACTIVITY = PERMISSION_WRITE_EXTERNAL_STORAGE; public static final int REQUEST_CODE_PERMISSION_BY_PASS = PERMISSION_BYPASSED; public static void logout(Context context, MXSession session, boolean clearCredentials) { if (session.isAlive()) { // stop the service EventStreamService eventStreamService = EventStreamService.getInstance(); ArrayList<String> matrixIds = new ArrayList<>(); matrixIds.add(session.getMyUserId()); eventStreamService.stopAccounts(matrixIds); // Publish to the server that we're now offline MyPresenceManager.getInstance(context, session).advertiseOffline(); MyPresenceManager.remove(session); // clear notification EventStreamService.removeNotification(); // unregister from the GCM. Matrix.getInstance(context).getSharedGCMRegistrationManager().unregister(session, null); // clear credentials Matrix.getInstance(context).clearSession(context, session, clearCredentials); } } public static boolean shouldRestartApp(Context context) { EventStreamService eventStreamService = EventStreamService.getInstance(); if (!Matrix.hasValidSessions()) { Log.e(LOG_TAG, "shouldRestartApp : the client has no valid session"); } if (null == eventStreamService) { Log.e(LOG_TAG, "eventStreamService is null : restart the event stream"); CommonActivityUtils.startEventStreamService(context); } return !Matrix.hasValidSessions(); } /** * With android M, the permissions kills the backgrounded application * and try to restart the last opened activity. * But, the sessions are not initialised (i.e the stores are not ready and so on). * Thus, the activity could have an invalid behaviour. * It seems safer to go to splash screen and to wait for the end of the initialisation. * * @param activity the caller activity * @return true if go to splash screen */ public static boolean isGoingToSplash(Activity activity) { return isGoingToSplash(activity, null, null); } /** * With android M, the permissions kills the backgrounded application * and try to restart the last opened activity. * But, the sessions are not initialised (i.e the stores are not ready and so on). * Thus, the activity could have an invalid behaviour. * It seems safer to go to splash screen and to wait for the end of the initialisation. * * @param activity the caller activity * @param sessionId the session id * @param roomId the room id * @return true if go to splash screen */ public static boolean isGoingToSplash(Activity activity, String sessionId, String roomId) { if (Matrix.hasValidSessions()) { List<MXSession> sessions = Matrix.getInstance(activity).getSessions(); for (MXSession session : sessions) { if (session.isAlive() && !session.getDataHandler().getStore().isReady()) { Intent intent = new Intent(activity, SplashActivity.class); if ((null != sessionId) && (null != roomId)) { intent.putExtra(SplashActivity.EXTRA_MATRIX_ID, sessionId); intent.putExtra(SplashActivity.EXTRA_ROOM_ID, roomId); } activity.startActivity(intent); activity.finish(); return true; } } } return false; } private static final String RESTART_IN_PROGRESS_KEY = "RESTART_IN_PROGRESS_KEY"; /** * The application has been started */ public static void onApplicationStarted(Activity activity) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean(RESTART_IN_PROGRESS_KEY, false); editor.commit(); } /** * Restart the application after 100ms * * @param activity activity */ public static void restartApp(Activity activity) { // clear the preferences SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences.Editor editor = preferences.edit(); // use the preference to avoid infinite relaunch on some devices // the culprit activity is restarted when System.exit is called. // so called it once to fix it if (!preferences.getBoolean(RESTART_IN_PROGRESS_KEY, false)) { CommonActivityUtils.displayToast(activity.getApplicationContext(), "Restart the application (low memory)"); Log.e(LOG_TAG, "Kill the application"); editor.putBoolean(RESTART_IN_PROGRESS_KEY, true); editor.commit(); PendingIntent mPendingIntent = PendingIntent.getActivity(activity, 314159, new Intent(activity, LoginActivity.class), PendingIntent.FLAG_CANCEL_CURRENT); // so restart the application after 100ms AlarmManager mgr = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 50, mPendingIntent); System.exit(0); } else { Log.e(LOG_TAG, "The application is restarting, please wait !!"); activity.finish(); } } /** * Logout the current user. * Jump to the login page when the logout is done. * * @param activity the caller activity */ public static void logout(Activity activity) { logout(activity, true); } /** * Logout the current user. * * @param activity the caller activity * @param goToLoginPage true to jump to the login page */ public static void logout(final Activity activity, boolean goToLoginPage) { // if no activity is provided, use the application context instead. final Context context = (null == activity) ? VectorApp.getInstance().getApplicationContext() : activity; EventStreamService.removeNotification(); stopEventStream(context); try { ShortcutBadger.setBadge(context, 0); } catch (Exception e) { Log.d(LOG_TAG, "## logout(): Exception Msg=" + e.getMessage()); } // warn that the user logs out Collection<MXSession> sessions = Matrix.getMXSessions(context); for (MXSession session : sessions) { // Publish to the server that we're now offline MyPresenceManager.getInstance(context, session).advertiseOffline(); MyPresenceManager.remove(session); } // clear the preferences SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String homeServer = preferences.getString(LoginActivity.HOME_SERVER_URL_PREF, context.getResources().getString(R.string.default_hs_server_url)); String identityServer = preferences.getString(LoginActivity.IDENTITY_SERVER_URL_PREF, context.getResources().getString(R.string.default_identity_server_url)); Boolean useGa = GAHelper.useGA(context); SharedPreferences.Editor editor = preferences.edit(); editor.clear(); editor.putString(LoginActivity.HOME_SERVER_URL_PREF, homeServer); editor.putString(LoginActivity.IDENTITY_SERVER_URL_PREF, identityServer); editor.commit(); if (null != useGa) { GAHelper.setUseGA(context, useGa); } // reset the GCM Matrix.getInstance(context).getSharedGCMRegistrationManager().resetGCMRegistration(false); // clear the preferences when the application goes to the login screen. if (goToLoginPage) { Matrix.getInstance(context).getSharedGCMRegistrationManager().clearPreferences(); } // clear credentials Matrix.getInstance(context).clearSessions(context, true); // ensure that corrupted values are cleared Matrix.getInstance(context).getLoginStorage().clear(); // clear the tmp store list Matrix.getInstance(context).clearTmpStoresList(); // reset the contacts PIDsRetriever.getInstance().reset(); ContactsManager.getInstance().reset(); MXMediasCache.clearThumbnailsCache(context); if (goToLoginPage) { if (null != activity) { // go to login page activity.startActivity(new Intent(activity, LoginActivity.class)); activity.finish(); } else { Intent intent = new Intent(context, LoginActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(intent); } } } /** * Remove the http schemes from the URl passed in parameter * * @param aUrl URL to be parsed * @return the URL with the scheme removed */ public static String removeUrlScheme(String aUrl) { String urlRetValue = aUrl; if (null != aUrl) { // remove URL scheme if (aUrl.startsWith(HTTP_SCHEME)) { urlRetValue = aUrl.substring(HTTP_SCHEME.length()); } else if (aUrl.startsWith(HTTPS_SCHEME)) { urlRetValue = aUrl.substring(HTTPS_SCHEME.length()); } } return urlRetValue; } //============================================================================================================== // Events stream service //============================================================================================================== /** * Indicate if a user is logged out or not. If no default session is enabled, * no user is logged. * * @param aContext App context * @return true if no user is logged in, false otherwise */ private static boolean isUserLogout(Context aContext) { boolean retCode = false; if (null == aContext) { retCode = true; } else { if (null == Matrix.getInstance(aContext.getApplicationContext()).getDefaultSession()) { retCode = true; } } return retCode; } /** * Send an action to the events service. * * @param context the context. * @param action the action to send. */ 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); } else { Log.d(LOG_TAG, "## sendEventStreamAction(): \"" + action + "\" action not sent - user logged out"); } } /** * Stop the event stream. * * @param context the context. */ private static void stopEventStream(Context context) { Log.d(LOG_TAG, "stopEventStream"); sendEventStreamAction(context, EventStreamService.StreamAction.STOP); } /** * Pause the event stream. * * @param context the context. */ public static void pauseEventStream(Context context) { Log.d(LOG_TAG, "pauseEventStream"); sendEventStreamAction(context, EventStreamService.StreamAction.PAUSE); } /** * Resume the events stream * * @param context the context. */ public static void resumeEventStream(Context context) { Log.d(LOG_TAG, "resumeEventStream"); sendEventStreamAction(context, EventStreamService.StreamAction.RESUME); } /** * Trigger a event stream catchup i.e. there is only sync/ call. * * @param context the context. */ public static void catchupEventStream(Context context) { if (VectorApp.isAppInBackground()) { Log.d(LOG_TAG, "catchupEventStream"); sendEventStreamAction(context, EventStreamService.StreamAction.CATCHUP); } } /** * Warn the events stream that there was a GCM status update. * * @param context the context. */ public static void onGcmUpdate(Context context) { Log.d(LOG_TAG, "onGcmUpdate"); sendEventStreamAction(context, EventStreamService.StreamAction.GCM_STATUS_UPDATE); } /** * Start the events stream service. * * @param context the context. */ public static void startEventStreamService(Context context) { // the events stream service is launched // either the application has never be launched // or the service has been killed on low memory if (EventStreamService.getInstance() == null) { ArrayList<String> matrixIds = new ArrayList<>(); Collection<MXSession> sessions = Matrix.getInstance(context.getApplicationContext()).getSessions(); if ((null != sessions) && (sessions.size() > 0)) { Log.d(LOG_TAG, "restart EventStreamService"); for (MXSession session : sessions) { boolean isSessionReady = session.getDataHandler().getStore().isReady(); if (!isSessionReady) { session.getDataHandler().getStore().open(); } // session to activate matrixIds.add(session.getCredentials().userId); } Intent intent = new Intent(context, EventStreamService.class); intent.putExtra(EventStreamService.EXTRA_MATRIX_IDS, matrixIds.toArray(new String[matrixIds.size()])); intent.putExtra(EventStreamService.EXTRA_STREAM_ACTION, EventStreamService.StreamAction.START.ordinal()); context.startService(intent); } } } /** * Check if the user power level allows to update the room avatar. This is mainly used to * determine if camera permission must be checked or not. * * @param aRoom the room * @param aSession the session * @return true if the user power level allows to update the avatar, false otherwise. */ public static boolean isPowerLevelEnoughForAvatarUpdate(Room aRoom, MXSession aSession) { boolean canUpdateAvatarWithCamera = false; PowerLevels powerLevels; if ((null != aRoom) && (null != aSession)) { if (null != (powerLevels = aRoom.getLiveState().getPowerLevels())) { int powerLevel = powerLevels.getUserPowerLevel(aSession.getMyUserId()); // check the power level against avatar level canUpdateAvatarWithCamera = (powerLevel >= powerLevels .minimumPowerLevelForSendingEventAsStateEvent(Event.EVENT_TYPE_STATE_ROOM_AVATAR)); } } return canUpdateAvatarWithCamera; } /** * Check if the permissions provided in the list are granted. * This is an asynchronous method if permissions are requested, the final response * is provided in onRequestPermissionsResult(). In this case checkPermissions() * returns false. * <br>If checkPermissions() returns true, the permissions were already granted. * The permissions to be granted are given as bit map in aPermissionsToBeGrantedBitMap (ex: {@link #REQUEST_CODE_PERMISSION_TAKE_PHOTO}). * <br>aPermissionsToBeGrantedBitMap is passed as the request code in onRequestPermissionsResult(). * <p> * If a permission was already denied by the user, a popup is displayed to * explain why vector needs the corresponding permission. * * @param aPermissionsToBeGrantedBitMap the permissions bit map to be granted * @param aCallingActivity the calling Activity that is requesting the permissions * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow) */ public static boolean checkPermissions(final int aPermissionsToBeGrantedBitMap, final Activity aCallingActivity) { boolean isPermissionGranted = false; // sanity check if (null == aCallingActivity) { Log.w(LOG_TAG, "## checkPermissions(): invalid input data"); isPermissionGranted = false; } else if (REQUEST_CODE_PERMISSION_BY_PASS == aPermissionsToBeGrantedBitMap) { isPermissionGranted = true; } else if ((REQUEST_CODE_PERMISSION_TAKE_PHOTO != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_AUDIO_IP_CALL != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_VIDEO_IP_CALL != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_MEMBERS_SEARCH != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_HOME_ACTIVITY != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_MEMBER_DETAILS != aPermissionsToBeGrantedBitMap) && (REQUEST_CODE_PERMISSION_ROOM_DETAILS != aPermissionsToBeGrantedBitMap)) { Log.w(LOG_TAG, "## checkPermissions(): permissions to be granted are not supported"); isPermissionGranted = false; } else { List<String> permissionListAlreadyDenied = new ArrayList<>(); List<String> permissionsListToBeGranted = new ArrayList<>(); final List<String> finalPermissionsListToBeGranted; boolean isRequestPermissionRequired = false; Resources resource = aCallingActivity.getResources(); String explanationMessage = ""; String permissionType; // retrieve the permissions to be granted according to the request code bit map if (PERMISSION_CAMERA == (aPermissionsToBeGrantedBitMap & PERMISSION_CAMERA)) { permissionType = Manifest.permission.CAMERA; isRequestPermissionRequired |= updatePermissionsToBeGranted(aCallingActivity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType); } if (PERMISSION_RECORD_AUDIO == (aPermissionsToBeGrantedBitMap & PERMISSION_RECORD_AUDIO)) { permissionType = Manifest.permission.RECORD_AUDIO; isRequestPermissionRequired |= updatePermissionsToBeGranted(aCallingActivity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType); } if (PERMISSION_WRITE_EXTERNAL_STORAGE == (aPermissionsToBeGrantedBitMap & PERMISSION_WRITE_EXTERNAL_STORAGE)) { permissionType = Manifest.permission.WRITE_EXTERNAL_STORAGE; isRequestPermissionRequired |= updatePermissionsToBeGranted(aCallingActivity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType); } // the contact book access is requested for any android platforms // for android M, we use the system preferences // for android < M, we use a dedicated settings if (PERMISSION_READ_CONTACTS == (aPermissionsToBeGrantedBitMap & PERMISSION_READ_CONTACTS)) { permissionType = Manifest.permission.READ_CONTACTS; if (Build.VERSION.SDK_INT >= 23) { isRequestPermissionRequired |= updatePermissionsToBeGranted(aCallingActivity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType); } else { if (!ContactsManager.getInstance().isContactBookAccessRequested()) { isRequestPermissionRequired = true; permissionsListToBeGranted.add(permissionType); } } } finalPermissionsListToBeGranted = permissionsListToBeGranted; // if some permissions were already denied: display a dialog to the user before asking again.. // if some permissions were already denied: display a dialog to the user before asking again.. if (!permissionListAlreadyDenied.isEmpty()) { if (null != resource) { // add the user info text to be displayed to explain why the permission is required by the App if (aPermissionsToBeGrantedBitMap == REQUEST_CODE_PERMISSION_VIDEO_IP_CALL || aPermissionsToBeGrantedBitMap == REQUEST_CODE_PERMISSION_AUDIO_IP_CALL) { // Permission request for VOIP call if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA) && permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) { // Both missing explanationMessage += resource .getString(R.string.permissions_rationale_msg_camera_and_audio); } else if (permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) { // Audio missing explanationMessage += resource .getString(R.string.permissions_rationale_msg_record_audio); explanationMessage += resource .getString(R.string.permissions_rationale_msg_record_audio_explanation); } else if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)) { // Camera missing explanationMessage += resource.getString(R.string.permissions_rationale_msg_camera); explanationMessage += resource .getString(R.string.permissions_rationale_msg_camera_explanation); } } else { for (String permissionAlreadyDenied : permissionListAlreadyDenied) { if (Manifest.permission.CAMERA.equals(permissionAlreadyDenied)) { if (!TextUtils.isEmpty(explanationMessage)) { explanationMessage += "\n\n"; } explanationMessage += resource.getString(R.string.permissions_rationale_msg_camera); } else if (Manifest.permission.RECORD_AUDIO.equals(permissionAlreadyDenied)) { if (!TextUtils.isEmpty(explanationMessage)) { explanationMessage += "\n\n"; } explanationMessage += resource .getString(R.string.permissions_rationale_msg_record_audio); } else if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permissionAlreadyDenied)) { if (!TextUtils.isEmpty(explanationMessage)) { explanationMessage += "\n\n"; } explanationMessage += resource .getString(R.string.permissions_rationale_msg_storage); } else if (Manifest.permission.READ_CONTACTS.equals(permissionAlreadyDenied)) { if (!TextUtils.isEmpty(explanationMessage)) { explanationMessage += "\n\n"; } explanationMessage += resource .getString(R.string.permissions_rationale_msg_contacts); } else { Log.d(LOG_TAG, "## checkPermissions(): already denied permission not supported"); } } } } else { // fall back if resource is null.. very unlikely explanationMessage = "You are about to be asked to grant permissions..\n\n"; } // display the dialog with the info text AlertDialog.Builder permissionsInfoDialog = new AlertDialog.Builder(aCallingActivity); if (null != resource) { permissionsInfoDialog.setTitle(resource.getString(R.string.permissions_rationale_popup_title)); } permissionsInfoDialog.setMessage(explanationMessage); permissionsInfoDialog.setPositiveButton(aCallingActivity.getString(R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (!finalPermissionsListToBeGranted.isEmpty()) { ActivityCompat.requestPermissions(aCallingActivity, finalPermissionsListToBeGranted .toArray(new String[finalPermissionsListToBeGranted.size()]), aPermissionsToBeGrantedBitMap); } } }); Dialog dialog = permissionsInfoDialog.show(); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { CommonActivityUtils.displayToast(aCallingActivity, aCallingActivity.getString(R.string.missing_permissions_warning)); } }); } else { // some permissions are not granted, ask permissions if (isRequestPermissionRequired) { final String[] fPermissionsArrayToBeGranted = finalPermissionsListToBeGranted .toArray(new String[finalPermissionsListToBeGranted.size()]); // for android < M, we use a custom dialog to request the contacts book access. if (permissionsListToBeGranted.contains(Manifest.permission.READ_CONTACTS) && (Build.VERSION.SDK_INT < 23)) { AlertDialog.Builder permissionsInfoDialog = new AlertDialog.Builder(aCallingActivity); permissionsInfoDialog.setIcon(android.R.drawable.ic_dialog_info); if (null != resource) { permissionsInfoDialog .setTitle(resource.getString(R.string.permissions_rationale_popup_title)); } permissionsInfoDialog.setMessage( resource.getString(R.string.permissions_msg_contacts_warning_other_androids)); // gives the contacts book access permissionsInfoDialog.setPositiveButton(aCallingActivity.getString(R.string.yes), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ContactsManager.getInstance().setIsContactBookAccessAllowed(true); ActivityCompat.requestPermissions(aCallingActivity, fPermissionsArrayToBeGranted, aPermissionsToBeGrantedBitMap); } }); // or reject it permissionsInfoDialog.setNegativeButton(aCallingActivity.getString(R.string.no), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ContactsManager.getInstance().setIsContactBookAccessAllowed(false); ActivityCompat.requestPermissions(aCallingActivity, fPermissionsArrayToBeGranted, aPermissionsToBeGrantedBitMap); } }); permissionsInfoDialog.show(); } else { ActivityCompat.requestPermissions(aCallingActivity, fPermissionsArrayToBeGranted, aPermissionsToBeGrantedBitMap); } } else { // permissions were granted, start now.. isPermissionGranted = true; } } } return isPermissionGranted; } /** * Helper method used in {@link #checkPermissions(int, Activity)} to populate the list of the * permissions to be granted (aPermissionsListToBeGranted_out) and the list of the permissions already denied (aPermissionAlreadyDeniedList_out). * * @param aCallingActivity calling activity * @param aPermissionAlreadyDeniedList_out list to be updated with the permissions already denied by the user * @param aPermissionsListToBeGranted_out list to be updated with the permissions to be granted * @param permissionType the permission to be checked * @return true if the permission requires to be granted, false otherwise */ private static boolean updatePermissionsToBeGranted(final Activity aCallingActivity, List<String> aPermissionAlreadyDeniedList_out, List<String> aPermissionsListToBeGranted_out, final String permissionType) { boolean isRequestPermissionRequested = false; // add permission to be granted aPermissionsListToBeGranted_out.add(permissionType); if (PackageManager.PERMISSION_GRANTED != ContextCompat .checkSelfPermission(aCallingActivity.getApplicationContext(), permissionType)) { isRequestPermissionRequested = true; // add permission to the ones that were already asked to the user if (ActivityCompat.shouldShowRequestPermissionRationale(aCallingActivity, permissionType)) { aPermissionAlreadyDeniedList_out.add(permissionType); } } return isRequestPermissionRequested; } /** * Helper method to process {@link CommonActivityUtils#REQUEST_CODE_PERMISSION_AUDIO_IP_CALL} * on onRequestPermissionsResult() methods. * * @param aContext App context * @param aPermissions permissions list * @param aGrantResults permissions granted results * @return true if audio IP call is permitted, false otherwise */ public static boolean onPermissionResultAudioIpCall(Context aContext, String[] aPermissions, int[] aGrantResults) { boolean isPermissionGranted = false; try { if (Manifest.permission.RECORD_AUDIO.equals(aPermissions[0])) { if (PackageManager.PERMISSION_GRANTED == aGrantResults[0]) { Log.d(LOG_TAG, "## onPermissionResultAudioIpCall(): RECORD_AUDIO permission granted"); isPermissionGranted = true; } else { Log.d(LOG_TAG, "## onPermissionResultAudioIpCall(): RECORD_AUDIO permission not granted"); if (null != aContext) CommonActivityUtils.displayToast(aContext, aContext.getString(R.string.permissions_action_not_performed_missing_permissions)); } } } catch (Exception ex) { Log.d(LOG_TAG, "## onPermissionResultAudioIpCall(): Exception MSg=" + ex.getMessage()); } return isPermissionGranted; } /** * Helper method to process {@link CommonActivityUtils#REQUEST_CODE_PERMISSION_VIDEO_IP_CALL} * on onRequestPermissionsResult() methods. * For video IP calls, record audio and camera permissions are both mandatory. * * @param aContext App context * @param aPermissions permissions list * @param aGrantResults permissions granted results * @return true if video IP call is permitted, false otherwise */ public static boolean onPermissionResultVideoIpCall(Context aContext, String[] aPermissions, int[] aGrantResults) { boolean isPermissionGranted = false; int result = 0; try { for (int i = 0; i < aPermissions.length; i++) { Log.d(LOG_TAG, "## onPermissionResultVideoIpCall(): " + aPermissions[i] + "=" + aGrantResults[i]); if (Manifest.permission.CAMERA.equals(aPermissions[i])) { if (PackageManager.PERMISSION_GRANTED == aGrantResults[i]) { Log.d(LOG_TAG, "## onPermissionResultVideoIpCall(): CAMERA permission granted"); result++; } else { Log.w(LOG_TAG, "## onPermissionResultVideoIpCall(): CAMERA permission not granted"); } } if (Manifest.permission.RECORD_AUDIO.equals(aPermissions[i])) { if (PackageManager.PERMISSION_GRANTED == aGrantResults[i]) { Log.d(LOG_TAG, "## onPermissionResultVideoIpCall(): WRITE_EXTERNAL_STORAGE permission granted"); result++; } else { Log.w(LOG_TAG, "## onPermissionResultVideoIpCall(): RECORD_AUDIO permission not granted"); } } } // Video over IP requires, both Audio & Video ! if (2 == result) { isPermissionGranted = true; } else { Log.w(LOG_TAG, "## onPermissionResultVideoIpCall(): No permissions granted to IP call (video or audio)"); if (null != aContext) CommonActivityUtils.displayToast(aContext, aContext.getString(R.string.permissions_action_not_performed_missing_permissions)); } } catch (Exception ex) { Log.d(LOG_TAG, "## onPermissionResultVideoIpCall(): Exception MSg=" + ex.getMessage()); } return isPermissionGranted; } //============================================================================================================== // Room preview methods. //============================================================================================================== /** * Start a room activity in preview mode. * * @param fromActivity the caller activity. * @param roomPreviewData the room preview information */ public static void previewRoom(final Activity fromActivity, RoomPreviewData roomPreviewData) { if ((null != fromActivity) && (null != roomPreviewData)) { VectorRoomActivity.sRoomPreviewData = roomPreviewData; Intent intent = new Intent(fromActivity, VectorRoomActivity.class); intent.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomPreviewData.getRoomId()); intent.putExtra(VectorRoomActivity.EXTRA_ROOM_PREVIEW_ID, roomPreviewData.getRoomId()); intent.putExtra(VectorRoomActivity.EXTRA_EXPAND_ROOM_HEADER, true); fromActivity.startActivity(intent); } } /** * Helper method used to build an intent to trigger a room preview. * * @param aMatrixId matrix ID of the user * @param aRoomId room ID * @param aContext application context * @param aTargetActivity the activity set in the returned intent * @return a valid intent if operation succeed, null otherwise */ public static Intent buildIntentPreviewRoom(String aMatrixId, String aRoomId, Context aContext, Class<?> aTargetActivity) { Intent intentRetCode; // sanity check if ((null == aContext) || (null == aRoomId) || (null == aMatrixId)) { intentRetCode = null; } else { MXSession session; // get the session if (null == (session = Matrix.getInstance(aContext).getSession(aMatrixId))) { session = Matrix.getInstance(aContext).getDefaultSession(); } // check session validity if ((null == session) || !session.isAlive()) { intentRetCode = null; } else { String roomAlias = null; Room room = session.getDataHandler().getRoom(aRoomId); // get the room alias (if any) for the preview data if ((null != room) && (null != room.getLiveState())) { roomAlias = room.getLiveState().getAlias(); } intentRetCode = new Intent(aContext, aTargetActivity); // extra required by VectorRoomActivity intentRetCode.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, aRoomId); intentRetCode.putExtra(VectorRoomActivity.EXTRA_ROOM_PREVIEW_ID, aRoomId); intentRetCode.putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, aMatrixId); intentRetCode.putExtra(VectorRoomActivity.EXTRA_EXPAND_ROOM_HEADER, true); // extra only required by VectorFakeRoomPreviewActivity intentRetCode.putExtra(VectorRoomActivity.EXTRA_ROOM_PREVIEW_ROOM_ALIAS, roomAlias); } } return intentRetCode; } /** * Start a room activity in preview mode. * If the room is already joined, open it in edition mode. * * @param fromActivity the caller activity. * @param session the session * @param roomId the roomId * @param roomAlias the room alias * @param callback the operation callback */ public static void previewRoom(final Activity fromActivity, final MXSession session, final String roomId, final String roomAlias, final ApiCallback<Void> callback) { previewRoom(fromActivity, session, roomId, new RoomPreviewData(session, roomId, null, roomAlias, null), callback); } /** * Start a room activity in preview mode. * If the room is already joined, open it in edition mode. * * @param fromActivity the caller activity. * @param session the session * @param roomId the roomId * @param roomPreviewData the room preview data * @param callback the operation callback */ public static void previewRoom(final Activity fromActivity, final MXSession session, final String roomId, final RoomPreviewData roomPreviewData, final ApiCallback<Void> callback) { Room room = session.getDataHandler().getRoom(roomId, false); // if the room exists if (null != room) { // either the user is invited if (room.isInvited()) { Log.d(LOG_TAG, "previewRoom : the user is invited -> display the preview " + VectorApp.getCurrentActivity()); previewRoom(fromActivity, roomPreviewData); } else { Log.d(LOG_TAG, "previewRoom : open the room"); HashMap<String, Object> params = new HashMap<>(); params.put(VectorRoomActivity.EXTRA_MATRIX_ID, session.getMyUserId()); params.put(VectorRoomActivity.EXTRA_ROOM_ID, roomId); CommonActivityUtils.goToRoomPage(fromActivity, session, params); } if (null != callback) { callback.onSuccess(null); } } else { roomPreviewData.fetchPreviewData(new ApiCallback<Void>() { private void onDone() { if (null != callback) { callback.onSuccess(null); } previewRoom(fromActivity, roomPreviewData); } @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(); } }); } } //============================================================================================================== // Room jump methods. //============================================================================================================== /** * Start a room activity with the dedicated parameters. * Pop the activity to the homeActivity before pushing the new activity. * * @param fromActivity the caller activity. * @param params the room activity parameters */ public static void goToRoomPage(final Activity fromActivity, final Map<String, Object> params) { goToRoomPage(fromActivity, null, params); } /** * Start a room activity with the dedicated parameters. * Pop the activity to the homeActivity before pushing the new activity. * * @param fromActivity the caller activity. * @param session the session. * @param params the room activity parameters. */ public static void goToRoomPage(final Activity fromActivity, final MXSession session, final Map<String, Object> params) { final MXSession finalSession = (session == null) ? Matrix.getMXSession(fromActivity, (String) params.get(VectorRoomActivity.EXTRA_MATRIX_ID)) : session; // sanity check if ((null == finalSession) || !finalSession.isAlive()) { return; } String roomId = (String) params.get(VectorRoomActivity.EXTRA_ROOM_ID); Room room = finalSession.getDataHandler().getRoom(roomId); // do not open a leaving room. // it does not make. if ((null != room) && (room.isLeaving())) { return; } fromActivity.runOnUiThread(new Runnable() { @Override public void run() { // if the activity is not the home activity if (!(fromActivity instanceof VectorHomeActivity)) { // pop to the home activity Log.d(LOG_TAG, "## goToRoomPage(): start VectorHomeActivity.."); Intent intent = new Intent(fromActivity, VectorHomeActivity.class); intent.setFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(VectorHomeActivity.EXTRA_JUMP_TO_ROOM_PARAMS, (Serializable) params); fromActivity.startActivity(intent); } else { // already to the home activity // so just need to open the room activity Log.d(LOG_TAG, "## goToRoomPage(): already in VectorHomeActivity.."); Intent intent = new Intent(fromActivity, VectorRoomActivity.class); for (String key : params.keySet()) { Object value = params.get(key); if (value instanceof String) { intent.putExtra(key, (String) value); } else if (value instanceof Boolean) { intent.putExtra(key, (Boolean) value); } else if (value instanceof Parcelable) { intent.putExtra(key, (Parcelable) value); } } // try to find a displayed room name if (null == params.get(VectorRoomActivity.EXTRA_DEFAULT_NAME)) { Room room = finalSession.getDataHandler() .getRoom((String) params.get(VectorRoomActivity.EXTRA_ROOM_ID)); if ((null != room) && room.isInvited()) { String displayName = VectorUtils.getRoomDisplayName(fromActivity, finalSession, room); if (null != displayName) { intent.putExtra(VectorRoomActivity.EXTRA_DEFAULT_NAME, displayName); } } } fromActivity.startActivity(intent); } } }); } //============================================================================================================== // 1:1 Room methods. //============================================================================================================== /** * Return all the 1:1 rooms joined by the searched user and by the current logged in user. * This method go through all the rooms, and for each room, tests if the searched user * and the logged in user are present. * * @param aSession session * @param aSearchedUserId the searched user ID * @return an array containing the found rooms */ private static ArrayList<Room> findOneToOneRoomList(final MXSession aSession, final String aSearchedUserId) { ArrayList<Room> listRetValue = new ArrayList<>(); List<RoomMember> roomMembersList; String userId0, userId1; if ((null != aSession) && (null != aSearchedUserId)) { Collection<Room> roomsList = aSession.getDataHandler().getStore().getRooms(); for (Room room : roomsList) { roomMembersList = (List<RoomMember>) room.getJoinedMembers(); if ((null != roomMembersList) && (ROOM_SIZE_ONE_TO_ONE == roomMembersList.size())) { userId0 = roomMembersList.get(0).getUserId(); userId1 = roomMembersList.get(1).getUserId(); // add the room where the second member is the searched one if (userId0.equals(aSearchedUserId) || userId1.equals(aSearchedUserId)) { listRetValue.add(room); } } } } return listRetValue; } /** * Return the 1:1 room with the most recent message, that the searched user and the current * logged user have joined. * Among the list of the 1:1 rooms, joined by the user, the room with the most recent * posted message is chosen to be returned. * * @param aSession session * @param aSearchedUserId the searched user ID * @return 1:1 room joined by the user with the most recent message, null otherwise */ private static Room findLatestOneToOneRoom(final MXSession aSession, final String aSearchedUserId) { long serverTimeStamp = 0, newServerTimeStamp; RoomSummary summary; Room mostRecentRoomRetValue = null; IMXStore mStore = aSession.getDataHandler().getStore(); // get all the "one to one" rooms where the user has joined ArrayList<Room> roomsFoundList = findOneToOneRoomList(aSession, aSearchedUserId); // parse all the 1:1 rooms and take the one with the most recent message. if (!roomsFoundList.isEmpty()) { for (Room room : roomsFoundList) { summary = mStore.getSummary(room.getRoomId()); try { // test on the most recent time stamp if ((null != summary) && ((newServerTimeStamp = summary.getLatestReceivedEvent() .getOriginServerTs()) > serverTimeStamp)) { mostRecentRoomRetValue = room; serverTimeStamp = newServerTimeStamp; } } catch (Exception ex) { Log.e(LOG_TAG, "## findLatestOneToOneRoom(): Exception Msg=" + ex.getMessage()); } } } return mostRecentRoomRetValue; } /** * Check if the room is a 1:1 room and if the searched user has joined this room. * The user ID is searched in the room only if the room is a 1:1 room. * This method is useful to check if we can create a new 1:1 room when it is * asked from a already existing room (see {@link VectorMemberDetailsActivity#ITEM_ACTION_START_CHAT}). * * @param aRoom room to be checked * @param aSearchedUserId the user ID to be searched in the room * @return true if the room is a 1:1 room where the user ID is present, false otherwise */ public static boolean isOneToOneRoomJoinedByUserId(final Room aRoom, final String aSearchedUserId) { boolean retVal = false; List<RoomMember> memberList; if ((null != aRoom) && (null != (memberList = (List<RoomMember>) aRoom.getJoinedMembers()))) { if (CommonActivityUtils.ROOM_SIZE_ONE_TO_ONE == memberList.size()) { for (RoomMember member : memberList) { if (member.getUserId().equals(aSearchedUserId)) { retVal = true; } } } } return retVal; } /** * Set a room as a direct chat room.<br> * In case of success the corresponding room is displayed. * * @param aSession session * @param aRoomId room ID * @param aParticipantUserId the direct chat invitee user ID * @param fromActivity calling activity * @param callback async response handler */ public static void setToggleDirectMessageRoom(final MXSession aSession, final String aRoomId, String aParticipantUserId, final Activity fromActivity, final ApiCallback<Void> callback) { if ((null == aSession) || (null == fromActivity) || TextUtils.isEmpty(aRoomId)) { Log.d(LOG_TAG, "## setToggleDirectMessageRoom(): failure - invalid input parameters"); } else { aSession.toggleDirectChatRoom(aRoomId, aParticipantUserId, new ApiCallback<Void>() { @Override public void onSuccess(Void info) { callback.onSuccess(null); } @Override public void onNetworkError(Exception e) { Log.d(LOG_TAG, "## setToggleDirectMessageRoom(): invite() onNetworkError Msg=" + e.getLocalizedMessage()); if (null != callback) { callback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { Log.d(LOG_TAG, "## setToggleDirectMessageRoom(): invite() onMatrixError Msg=" + e.getLocalizedMessage()); if (null != callback) { callback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { Log.d(LOG_TAG, "## setToggleDirectMessageRoom(): invite() onUnexpectedError Msg=" + e.getLocalizedMessage()); if (null != callback) { callback.onUnexpectedError(e); } } }); } } /** * Offer to send some dedicated intent data to an existing room * * @param fromActivity the caller activity * @param intent the intent param */ public static void sendFilesTo(final Activity fromActivity, final Intent intent) { if (Matrix.getMXSessions(fromActivity).size() == 1) { sendFilesTo(fromActivity, intent, Matrix.getMXSession(fromActivity, null)); } else if (fromActivity instanceof FragmentActivity) { FragmentManager fm = ((FragmentActivity) fromActivity).getSupportFragmentManager(); AccountsSelectionDialogFragment fragment = (AccountsSelectionDialogFragment) fm .findFragmentByTag(MXCActionBarActivity.TAG_FRAGMENT_ACCOUNT_SELECTION_DIALOG); if (fragment != null) { fragment.dismissAllowingStateLoss(); } fragment = AccountsSelectionDialogFragment.newInstance(Matrix.getMXSessions(fromActivity)); fragment.setListener(new AccountsSelectionDialogFragment.AccountsListener() { @Override public void onSelected(final MXSession session) { fromActivity.runOnUiThread(new Runnable() { @Override public void run() { sendFilesTo(fromActivity, intent, session); } }); } }); fragment.show(fm, MXCActionBarActivity.TAG_FRAGMENT_ACCOUNT_SELECTION_DIALOG); } } /** * Offer to send some dedicated intent data to an existing room * * @param fromActivity the caller activity * @param intent the intent param * @param session the session/ */ private static void sendFilesTo(final Activity fromActivity, final Intent intent, final MXSession session) { // sanity check if ((null == session) || !session.isAlive()) { return; } ArrayList<RoomSummary> mergedSummaries = new ArrayList<>( session.getDataHandler().getStore().getSummaries()); // keep only the joined room for (int index = 0; index < mergedSummaries.size(); index++) { RoomSummary summary = mergedSummaries.get(index); Room room = session.getDataHandler().getRoom(summary.getRoomId()); if ((null == room) || room.isInvited() || room.isConferenceUserRoom()) { mergedSummaries.remove(index); index--; } } Collections.sort(mergedSummaries, new Comparator<RoomSummary>() { @Override public int compare(RoomSummary lhs, RoomSummary rhs) { if (lhs == null || lhs.getLatestReceivedEvent() == null) { return 1; } else if (rhs == null || rhs.getLatestReceivedEvent() == null) { return -1; } if (lhs.getLatestReceivedEvent().getOriginServerTs() > rhs.getLatestReceivedEvent() .getOriginServerTs()) { return -1; } else if (lhs.getLatestReceivedEvent().getOriginServerTs() < rhs.getLatestReceivedEvent() .getOriginServerTs()) { return 1; } return 0; } }); AlertDialog.Builder builderSingle = new AlertDialog.Builder(fromActivity); builderSingle.setTitle(fromActivity.getText(R.string.send_files_in)); VectorRoomsSelectionAdapter adapter = new VectorRoomsSelectionAdapter(fromActivity, R.layout.adapter_item_vector_recent_room, session); adapter.addAll(mergedSummaries); builderSingle.setNegativeButton(fromActivity.getText(R.string.cancel), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); final ArrayList<RoomSummary> fMergedSummaries = mergedSummaries; builderSingle.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, final int which) { dialog.dismiss(); fromActivity.runOnUiThread(new Runnable() { @Override public void run() { RoomSummary summary = fMergedSummaries.get(which); HashMap<String, Object> params = new HashMap<>(); params.put(VectorRoomActivity.EXTRA_MATRIX_ID, session.getMyUserId()); params.put(VectorRoomActivity.EXTRA_ROOM_ID, summary.getRoomId()); params.put(VectorRoomActivity.EXTRA_ROOM_INTENT, intent); CommonActivityUtils.goToRoomPage(fromActivity, session, params); } }); } }); builderSingle.show(); } //============================================================================================================== // Parameters checkers. //============================================================================================================== //============================================================================================================== // Media utils //============================================================================================================== /** * Save a media in the downloads directory and offer to open it with a third party application. * * @param activity the activity * @param savedMediaPath the media path * @param mimeType the media mime type. */ public static void openMedia(final Activity activity, final String savedMediaPath, final String mimeType) { if ((null != activity) && (null != savedMediaPath)) { activity.runOnUiThread(new Runnable() { @Override public void run() { try { File file = new File(savedMediaPath); Intent intent = new Intent(); intent.setAction(android.content.Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(file), mimeType); activity.startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(activity, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } catch (Exception e) { Log.d(LOG_TAG, "## openMedia(): Exception Msg=" + e.getMessage()); } } }); } } /** * Copy a file into a dstPath directory. * The output filename can be provided. * The output file is not overridden if it is already exist. * * @param sourceFile the file source path * @param dstDirPath the dst path * @param outputFilename optional the output filename * @return the downloads file path if the file exists or has been properly saved */ private static String saveFileInto(File sourceFile, String dstDirPath, String outputFilename) { // sanity check if ((null == sourceFile) || (null == dstDirPath)) { return null; } // defines another name for the external media String dstFileName; // build a filename is not provided if (null == outputFilename) { // extract the file extension from the uri int dotPos = sourceFile.getName().lastIndexOf("."); String fileExt = ""; if (dotPos > 0) { fileExt = sourceFile.getName().substring(dotPos); } dstFileName = "vector_" + System.currentTimeMillis() + fileExt; } else { dstFileName = outputFilename; } File dstDir = Environment.getExternalStoragePublicDirectory(dstDirPath); if (dstDir != null) { dstDir.mkdirs(); } File dstFile = new File(dstDir, dstFileName); // if the file already exists, append a marker if (dstFile.exists()) { String baseFileName = dstFileName; String fileExt = ""; int lastDotPos = dstFileName.lastIndexOf("."); if (lastDotPos > 0) { baseFileName = dstFileName.substring(0, lastDotPos); fileExt = dstFileName.substring(lastDotPos); } int counter = 1; while (dstFile.exists()) { dstFile = new File(dstDir, baseFileName + "(" + counter + ")" + fileExt); counter++; } } // Copy source file to destination FileInputStream inputStream = null; FileOutputStream outputStream = null; try { dstFile.createNewFile(); inputStream = new FileInputStream(sourceFile); outputStream = new FileOutputStream(dstFile); byte[] buffer = new byte[1024 * 10]; int len; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); } } catch (Exception e) { dstFile = null; } finally { // Close resources try { if (inputStream != null) inputStream.close(); if (outputStream != null) outputStream.close(); } catch (Exception e) { Log.e(LOG_TAG, "## saveFileInto(): Exception Msg=" + e.getMessage()); } } if (null != dstFile) { return dstFile.getAbsolutePath(); } else { return null; } } /** * Save a media URI into the download directory * * @param context the context * @param srcFile the source file. * @param filename the filename (optional) * @return the downloads file path */ @SuppressLint("NewApi") public static String saveMediaIntoDownloads(Context context, File srcFile, String filename, String mimeType) { String fullFilePath = saveFileInto(srcFile, Environment.DIRECTORY_DOWNLOADS, filename); 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()); } } } return fullFilePath; } //============================================================================================================== // toast utils //============================================================================================================== /** * Helper method to display a toast message. * * @param aCallingActivity calling Activity instance * @param aMsgToDisplay message to display */ public static void displayToastOnUiThread(final Activity aCallingActivity, final String aMsgToDisplay) { if (null != aCallingActivity) { aCallingActivity.runOnUiThread(new Runnable() { @Override public void run() { CommonActivityUtils.displayToast(aCallingActivity.getApplicationContext(), aMsgToDisplay); } }); } } /** * Display a toast * * @param aContext the context. * @param aTextToDisplay the text to display. */ public static void displayToast(Context aContext, CharSequence aTextToDisplay) { Toast.makeText(aContext, aTextToDisplay, Toast.LENGTH_SHORT).show(); } /** * Display a snack. * * @param aTargetView the parent view. * @param aTextToDisplay the text to display. */ public static void displaySnack(View aTargetView, CharSequence aTextToDisplay) { Snackbar.make(aTargetView, aTextToDisplay, Snackbar.LENGTH_SHORT).show(); } //============================================================================================================== // call utils //============================================================================================================== /** * Display a toast message according to the end call reason. * * @param aCallingActivity calling activity * @param aCallEndReason define the reason of the end call */ public static void processEndCallInfo(Activity aCallingActivity, int aCallEndReason) { if (null != aCallingActivity) { if (IMXCall.END_CALL_REASON_UNDEFINED != aCallEndReason) { switch (aCallEndReason) { case IMXCall.END_CALL_REASON_PEER_HANG_UP: if (aCallingActivity instanceof InComingCallActivity) { CommonActivityUtils.displayToastOnUiThread(aCallingActivity, aCallingActivity.getString(R.string.call_error_peer_cancelled_call)); } else { // let VectorCallActivity manage its } break; case IMXCall.END_CALL_REASON_PEER_HANG_UP_ELSEWHERE: CommonActivityUtils.displayToastOnUiThread(aCallingActivity, aCallingActivity.getString(R.string.call_error_peer_hangup_elsewhere)); break; default: break; } } } } //============================================================================================================== // room utils //============================================================================================================== /** * Helper method to retrieve the max power level contained in the room. * This value is used to indicate what is the power level value required * to be admin of the room. * * @return max power level of the current room */ public static int getRoomMaxPowerLevel(Room aRoom) { int maxPowerLevel = 0; if (null != aRoom) { PowerLevels powerLevels = aRoom.getLiveState().getPowerLevels(); if (null != powerLevels) { int tempPowerLevel; // find out the room member Collection<RoomMember> members = aRoom.getMembers(); for (RoomMember member : members) { tempPowerLevel = powerLevels.getUserPowerLevel(member.getUserId()); if (tempPowerLevel > maxPowerLevel) { maxPowerLevel = tempPowerLevel; } } } } return maxPowerLevel; } //============================================================================================================== // Application badge (displayed in the launcher) //============================================================================================================== private static int mBadgeValue = 0; /** * Update the application badge value. * * @param context the context * @param badgeValue the new badge value */ public static void updateBadgeCount(Context context, int badgeValue) { try { mBadgeValue = badgeValue; ShortcutBadger.setBadge(context, badgeValue); } catch (Exception e) { Log.e(LOG_TAG, "## updateBadgeCount(): Exception Msg=" + e.getMessage()); } } /** * @return the badge value */ public static int getBadgeCount() { return mBadgeValue; } /** * Refresh the badge count for specific configurations.<br> * The refresh is only effective if the device is: * <ul><li>offline</li><li>does not support GCM</li> * <li>GCM registration failed</li> * <br>Notifications rooms are parsed to track the notification count value. * * @param aSession session value * @param aContext App context */ public static void specificUpdateBadgeUnreadCount(MXSession aSession, Context aContext) { MXDataHandler dataHandler; // sanity check if ((null == aContext) || (null == aSession)) { Log.w(LOG_TAG, "## specificUpdateBadgeUnreadCount(): invalid input null values"); } else if ((null == (dataHandler = aSession.getDataHandler()))) { Log.w(LOG_TAG, "## specificUpdateBadgeUnreadCount(): invalid DataHandler instance"); } else { if (aSession.isAlive()) { boolean isRefreshRequired; GcmRegistrationManager gcmMgr = Matrix.getInstance(aContext).getSharedGCMRegistrationManager(); // update the badge count if the device is offline, GCM is not supported or GCM registration failed isRefreshRequired = !Matrix.getInstance(aContext).isConnected(); isRefreshRequired |= (null != gcmMgr) && (!gcmMgr.useGCM() || !gcmMgr.hasRegistrationToken()); if (isRefreshRequired) { updateBadgeCount(aContext, dataHandler); } } } } /** * Update the badge count value according to the rooms content. * * @param aContext App context * @param aDataHandler data handler instance */ public static void updateBadgeCount(Context aContext, MXDataHandler aDataHandler) { //sanity check if ((null == aContext) || (null == aDataHandler)) { Log.w(LOG_TAG, "## updateBadgeCount(): invalid input null values"); } else if (null == aDataHandler.getStore()) { Log.w(LOG_TAG, "## updateBadgeCount(): invalid store instance"); } else { ArrayList<Room> roomCompleteList = new ArrayList<>(aDataHandler.getStore().getRooms()); int unreadRoomsCount = 0; // compute the number of rooms with unread notifications // "invite to join a room" counts as a notification SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(aContext); boolean isInvitedNotifEnabled = preferences .getBoolean(aContext.getResources().getString(R.string.settings_invited_to_room), false); for (Room room : roomCompleteList) { if ((room.getNotificationCount() > 0) || (isInvitedNotifEnabled && room.isInvited())) { unreadRoomsCount++; } } // update the badge counter Log.d(LOG_TAG, "## updateBadgeCount(): badge update count=" + unreadRoomsCount); CommonActivityUtils.updateBadgeCount(aContext, unreadRoomsCount); } } //============================================================================================================== // Low memory management //============================================================================================================== private static final String LOW_MEMORY_LOG_TAG = "Memory usage"; /** * Log the memory statuses. * * @param activity the calling activity * @return if the device is running on low memory. */ public static boolean displayMemoryInformation(Activity activity, String title) { long freeSize = 0L; long totalSize = 0L; long usedSize = -1L; try { Runtime info = Runtime.getRuntime(); freeSize = info.freeMemory(); totalSize = info.totalMemory(); usedSize = totalSize - freeSize; } catch (Exception e) { e.printStackTrace(); } Log.e(LOW_MEMORY_LOG_TAG, "---------------------------------------------------"); Log.e(LOW_MEMORY_LOG_TAG, "----------- " + title + " -----------------"); Log.e(LOW_MEMORY_LOG_TAG, "---------------------------------------------------"); Log.e(LOW_MEMORY_LOG_TAG, "usedSize " + (usedSize / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "freeSize " + (freeSize / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "totalSize " + (totalSize / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "---------------------------------------------------"); if (null != activity) { ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); ActivityManager activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); activityManager.getMemoryInfo(mi); Log.e(LOW_MEMORY_LOG_TAG, "availMem " + (mi.availMem / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "totalMem " + (mi.totalMem / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "threshold " + (mi.threshold / 1048576L) + " MB"); Log.e(LOW_MEMORY_LOG_TAG, "lowMemory " + (mi.lowMemory)); Log.e(LOW_MEMORY_LOG_TAG, "---------------------------------------------------"); return mi.lowMemory; } else { return false; } } /** * Manage the low memory case * * @param activity activity instance */ public static void onLowMemory(Activity activity) { if (!VectorApp.isAppInBackground()) { String activityName = (null != activity) ? activity.getClass().getSimpleName() : "NotAvailable"; Log.e(LOW_MEMORY_LOG_TAG, "Active application : onLowMemory from " + activityName); // it seems that onLowMemory is called whereas the device is seen on low memory condition // so, test if the both conditions if (displayMemoryInformation(activity, "onLowMemory test")) { if (CommonActivityUtils.shouldRestartApp(activity)) { Log.e(LOW_MEMORY_LOG_TAG, "restart"); CommonActivityUtils.restartApp(activity); } else { Log.e(LOW_MEMORY_LOG_TAG, "clear the application cache"); Matrix.getInstance(activity).reloadSessions(activity); } } else { Log.e(LOW_MEMORY_LOG_TAG, "Wait to be concerned"); } } else { Log.e(LOW_MEMORY_LOG_TAG, "background application : onLowMemory "); } displayMemoryInformation(activity, "onLowMemory global"); } /** * Manage the trim memory. * * @param activity the activity. * @param level the memory level */ public static void onTrimMemory(Activity activity, int level) { String activityName = (null != activity) ? activity.getClass().getSimpleName() : "NotAvailable"; Log.e(LOW_MEMORY_LOG_TAG, "Active application : onTrimMemory from " + activityName + " level=" + level); // TODO implement things to reduce memory usage displayMemoryInformation(activity, "onTrimMemory"); } //============================================================================================================== // e2e devices management //============================================================================================================== /** * Display the device verification warning * * @param deviceInfo the device info */ static public <T> void displayDeviceVerificationDialog(final MXDeviceInfo deviceInfo, final String sender, final MXSession session, Activity activiy, final ApiCallback<Void> callback) { // sanity check if ((null == deviceInfo) || (null == sender) || (null == session)) { Log.e(LOG_TAG, "## displayDeviceVerificationDialog(): invalid imput parameters"); return; } android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder( activiy); LayoutInflater inflater = activiy.getLayoutInflater(); View layout = inflater.inflate(R.layout.encrypted_verify_device, null); TextView textView; textView = (TextView) layout.findViewById(R.id.encrypted_device_info_device_name); textView.setText(deviceInfo.displayName()); textView = (TextView) layout.findViewById(R.id.encrypted_device_info_device_id); textView.setText(deviceInfo.deviceId); textView = (TextView) layout.findViewById(R.id.encrypted_device_info_device_key); textView.setText(deviceInfo.fingerprint()); builder.setView(layout); builder.setTitle(R.string.encryption_information_verify_device); builder.setPositiveButton(R.string.encryption_information_verify_key_match, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { session.getCrypto().setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, deviceInfo.deviceId, sender, callback); } }); builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.create().show(); } /** * Export the e2e keys for a dedicated session. * @param session the session * @param password the password * @param callback the asynchronous callback. */ public static void exportKeys(final MXSession session, final String password, final ApiCallback<String> callback) { final Context appContext = VectorApp.getInstance(); session.getCrypto().exportRoomKeys(password, new ApiCallback<byte[]>() { @Override public void onSuccess(byte[] bytesArray) { try { ByteArrayInputStream stream = new ByteArrayInputStream(bytesArray); String url = session.getMediasCache().saveMedia(stream, "riot-" + System.currentTimeMillis() + ".txt", "text/plain"); stream.close(); String path = CommonActivityUtils.saveMediaIntoDownloads(appContext, new File(Uri.parse(url).getPath()), "riot-keys.txt", "text/plain"); if (null != callback) { callback.onSuccess(path); } } catch (Exception e) { if (null != callback) { callback.onMatrixError(new MatrixError(null, e.getLocalizedMessage())); } } } @Override public void onNetworkError(Exception e) { if (null != callback) { callback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { if (null != callback) { callback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { if (null != callback) { callback.onUnexpectedError(e); } } }); } private static final String TAG_FRAGMENT_UNKNOWN_DEVICES_DIALOG_DIALOG = "ActionBarActivity.TAG_FRAGMENT_UNKNOWN_DEVICES_DIALOG_DIALOG"; /** * Display the unknown e2e devices * @param session the session * @param activity the calling activity * @param unknownDevices the unknown devices list * @param listener optional listener to add an optional "Send anyway" button */ public static void displayUnknownDevicesDialog(MXSession session, FragmentActivity activity, MXUsersDevicesMap<MXDeviceInfo> unknownDevices, VectorUnknownDevicesFragment.IUnknownDevicesSendAnywayListener listener) { // sanity checks if ((null == unknownDevices) || (0 == unknownDevices.getMap().size())) { return; } FragmentManager fm = activity.getSupportFragmentManager(); VectorUnknownDevicesFragment fragment = (VectorUnknownDevicesFragment) fm .findFragmentByTag(TAG_FRAGMENT_UNKNOWN_DEVICES_DIALOG_DIALOG); if (fragment != null) { fragment.dismissAllowingStateLoss(); } fragment = VectorUnknownDevicesFragment.newInstance(session.getMyUserId(), unknownDevices, listener); fragment.show(fm, TAG_FRAGMENT_UNKNOWN_DEVICES_DIALOG_DIALOG); } }