This is an old revision of the document!
/* * Headwind Remote: Open Source Remote Access Software for Android * https://headwind-remote.com * * Copyright © 2022 headwind-remote.com * * 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 com.hmdm.control;
import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.PixelFormat; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.hmdm.control.janus.SharingEngineJanus;
public class MainActivity extends AppCompatActivity implements SharingEngineJanus.EventListener, SharingEngineJanus.StateListener {
private ImageView imageViewConnStatus; private TextView textViewConnStatus; private EditText editTextSessionId; private EditText editTextPassword; private TextView textViewComment; private TextView textViewConnect; private TextView textViewSendLink; private TextView textViewExit;
private ImageView overlayDot; private Handler handler = new Handler(); private int overlayDotAlpha; private int overlayDotDirection = 1;
private Dialog exitOnIdleDialog; private int exitCounter; private static final int EXIT_PROMPT_SEC = 10;
private static final int OVERLAY_DOT_ANIMATION_INCREMENT = 20; private static final int OVERLAY_DOT_ANIMATION_DELAY = 200;
private SharingEngine sharingEngine;
private SettingsHelper settingsHelper;
private String sessionId; private String password; private String adminName;
private final static String ATTR_SESSION_ID = "sessionId"; private final static String ATTR_PASSWORD = "password"; private final static String ATTR_ADMIN_NAME = "adminName";
private boolean needReconnect = false;
private MediaProjectionManager projectionManager;
private BroadcastReceiver mSharingServiceReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent == null || intent.getAction() == null) { return; } if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_START)) { notifySharingStart();
} else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_STOP)) { notifySharingStop(); adminName = null; updateUI(); cancelSharingTimeout(); scheduleExitOnIdle();
} else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_FAILED)) { String message = intent.getStringExtra(Const.EXTRA_MESSAGE); if (message != null) { Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); } adminName = null; updateUI(); cancelSharingTimeout(); scheduleExitOnIdle();
} else if (intent.getAction().equals(Const.ACTION_CONNECTION_FAILURE)) { sharingEngine.setState(Const.STATE_DISCONNECTED); Toast.makeText(MainActivity.this, R.string.connection_failure_hint, Toast.LENGTH_LONG).show(); updateUI();
} else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_PERMISSION_NEEDED)) { startActivityForResult(projectionManager.createScreenCaptureIntent(), Const.REQUEST_SCREEN_SHARE); } } };
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
if (savedInstanceState != null) { restoreInstanceState(savedInstanceState); }
Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); settingsHelper = SettingsHelper.getInstance(this); sharingEngine = SharingEngineFactory.getSharingEngine(); sharingEngine.setEventListener(this); sharingEngine.setStateListener(this);
DisplayMetrics metrics = new DisplayMetrics(); ScreenSharingHelper.getRealScreenSize(this, metrics); float videoScale = ScreenSharingHelper.adjustScreenMetrics(metrics); settingsHelper.setFloat(SettingsHelper.KEY_VIDEO_SCALE, videoScale); ScreenSharingHelper.setScreenMetrics(this, metrics.widthPixels, metrics.heightPixels, metrics.densityDpi);
sharingEngine.setScreenWidth(metrics.widthPixels); sharingEngine.setScreenHeight(metrics.heightPixels);
IntentFilter intentFilter = new IntentFilter(Const.ACTION_SCREEN_SHARING_START); intentFilter.addAction(Const.ACTION_SCREEN_SHARING_STOP); intentFilter.addAction(Const.ACTION_SCREEN_SHARING_PERMISSION_NEEDED); intentFilter.addAction(Const.ACTION_SCREEN_SHARING_FAILED); intentFilter.addAction(Const.ACTION_CONNECTION_FAILURE); LocalBroadcastManager.getInstance(this).registerReceiver(mSharingServiceReceiver, intentFilter);
projectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE);
initUI(); setDefaultSettings(); }
@Override protected void onResume() { super.onResume(); updateUI();
startService(new Intent(MainActivity.this, GestureDispatchService.class));
connect();
checkAccessibility(); }
private void checkAccessibility() { if (!Utils.isAccessibilityPermissionGranted(this)) { textViewConnect.setVisibility(View.INVISIBLE); new AlertDialog.Builder(this) .setMessage(R.string.accessibility_hint) .setPositiveButton(R.string.continue_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS); try { startActivityForResult(intent, 0); } catch (Exception e) { // Accessibility settings cannot be opened reportAccessibilityUnavailable(); } } }) .setCancelable(false) .create() .show(); } else { configureAndConnect(); } }
private void reportAccessibilityUnavailable() { new AlertDialog.Builder(this) .setMessage(R.string.accessibility_unavailable_error) .setPositiveButton(R.string.button_exit, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { exitApp(); } }) .setCancelable(false) .create() .show(); }
private void configureAndConnect() { if (settingsHelper.getString(SettingsHelper.KEY_SERVER_URL) == null) { // Not configured yet settingsHelper.setString(SettingsHelper.KEY_SERVER_URL, BuildConfig.DEFAULT_SERVER_URL); settingsHelper.setString(SettingsHelper.KEY_SECRET, BuildConfig.DEFAULT_SECRET); settingsHelper.setBoolean(SettingsHelper.KEY_USE_DEFAULT, !BuildConfig.DEFAULT_SECRET.equals("")); Intent intent = new Intent(this, SettingsActivity.class); startActivityForResult(intent, Const.REQUEST_SETTINGS); return; }
if (needReconnect) { // Here we go after changing settings needReconnect = false; if (sharingEngine.getState() != Const.STATE_DISCONNECTED) { sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> connect()); } else { connect(); } } else { if (sharingEngine.getState() == Const.STATE_DISCONNECTED && sharingEngine.getErrorReason() == null) { connect(); } } }
@Override public void onDestroy() { try { LocalBroadcastManager.getInstance(this).unregisterReceiver(mSharingServiceReceiver); } catch (Exception e) { } super.onDestroy(); }
@Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putString(ATTR_SESSION_ID, sessionId); savedInstanceState.putString(ATTR_PASSWORD, password); savedInstanceState.putString(ATTR_ADMIN_NAME, adminName); super.onSaveInstanceState(savedInstanceState); }
private void restoreInstanceState(Bundle savedInstanceState) { sessionId = savedInstanceState.getString(ATTR_SESSION_ID); password = savedInstanceState.getString(ATTR_PASSWORD); adminName = savedInstanceState.getString(ATTR_ADMIN_NAME); }
@Override public void onBackPressed() { Toast.makeText(this, R.string.back_pressed, Toast.LENGTH_LONG).show(); }
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; }
@Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId();
if (id == R.id.action_settings) { if (adminName != null) { Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_LONG).show(); return true; } Intent intent = new Intent(this, SettingsActivity.class); startActivityForResult(intent, Const.REQUEST_SETTINGS); cancelExitOnIdle(); return true; } else if (id == R.id.action_about) { showAbout(); return true; }
return super.onOptionsItemSelected(item); }
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data);
if (requestCode == Const.REQUEST_SETTINGS) { if (resultCode == Const.RESULT_DIRTY) { needReconnect = true; } else { scheduleExitOnIdle(); } } else if (requestCode == Const.REQUEST_SCREEN_SHARE) { if (resultCode != RESULT_OK) { Toast.makeText(this, R.string.screen_cast_denied, Toast.LENGTH_LONG).show(); adminName = null; updateUI(); cancelSharingTimeout(); scheduleExitOnIdle(); } else { ScreenSharingHelper.startSharing(this, resultCode, data); } } }
private void initUI() { imageViewConnStatus = findViewById(R.id.image_conn_status); textViewConnStatus = findViewById(R.id.conn_status); editTextSessionId = findViewById(R.id.session_id_edit); editTextPassword = findViewById(R.id.password_edit); textViewComment = findViewById(R.id.comment); textViewConnect = findViewById(R.id.reconnect); textViewSendLink = findViewById(R.id.send_link); textViewExit = findViewById(R.id.disconnect_exit);
textViewConnect.setOnClickListener(v -> connect());
textViewSendLink.setOnClickListener(v -> sendLink());
textViewExit.setOnClickListener(v -> gracefulExit()); }
private void gracefulExit() { if (adminName != null) { notifySharingStop(); ScreenSharingHelper.stopSharing(MainActivity.this, true); } sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> exitApp()); // 5 sec timeout to exit handler.postDelayed(() -> exitApp(), 5000); }
private void exitApp() { Intent intent = new Intent(MainActivity.this, ScreenSharingService.class); stopService(intent); intent = new Intent(MainActivity.this, GestureDispatchService.class); stopService(intent); finishAffinity(); System.exit(0); }
private void updateUI() { int[] stateLabels = {R.string.state_disconnected, R.string.state_connecting, R.string.state_connected, R.string.state_sharing, R.string.state_disconnecting}; int[] stateImages = {R.drawable.ic_disconnected, R.drawable.ic_connecting, R.drawable.ic_connected, R.drawable.ic_sharing, R.drawable.ic_connecting};
int state = sharingEngine.getState(); if (state == Const.STATE_CONNECTED && adminName != null) { imageViewConnStatus.setImageDrawable(getDrawable(stateImages[Const.STATE_SHARING])); textViewConnStatus.setText(stateLabels[Const.STATE_SHARING]); } else { imageViewConnStatus.setImageDrawable(getDrawable(stateImages[state])); textViewConnStatus.setText(stateLabels[state]); } String serverUrl = Utils.prepareDisplayUrl(settingsHelper.getString(SettingsHelper.KEY_SERVER_URL));
textViewSendLink.setVisibility(state == Const.STATE_CONNECTED ? View.VISIBLE : View.INVISIBLE); textViewConnect.setVisibility(state == Const.STATE_DISCONNECTED ? View.VISIBLE : View.INVISIBLE); switch (state) { case Const.STATE_DISCONNECTED: editTextSessionId.setText(""); editTextPassword.setText(""); if (sharingEngine.getErrorReason() != null) { textViewComment.setText(getString(R.string.hint_connection_error, serverUrl)); } break; case Const.STATE_CONNECTING: textViewComment.setText(getString(R.string.hint_connecting, serverUrl)); break; case Const.STATE_DISCONNECTING: textViewComment.setText(getString(R.string.hint_disconnecting)); break; case Const.STATE_CONNECTED: editTextSessionId.setText(sessionId); editTextPassword.setText(password); textViewComment.setText(adminName != null ? getString(R.string.hint_sharing, adminName) : getString(R.string.hint_connected, serverUrl) ); break; } }
private void setDefaultSettings() { if (settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME) == null) { settingsHelper.setString(SettingsHelper.KEY_DEVICE_NAME, Build.MANUFACTURER + " " + Build.MODEL); } if (settingsHelper.getInt(SettingsHelper.KEY_BITRATE) == 0) { settingsHelper.setInt(SettingsHelper.KEY_BITRATE, Const.DEFAULT_BITRATE); } if (settingsHelper.getInt(SettingsHelper.KEY_FRAME_RATE) == 0) { settingsHelper.setInt(SettingsHelper.KEY_FRAME_RATE, Const.DEFAULT_FRAME_RATE); } if (settingsHelper.getInt(SettingsHelper.KEY_IDLE_TIMEOUT) == 0) { settingsHelper.setInt(SettingsHelper.KEY_IDLE_TIMEOUT, Const.DEFAULT_IDLE_TIMEOUT); } if (settingsHelper.getInt(SettingsHelper.KEY_PING_TIMEOUT) == 0) { settingsHelper.setInt(SettingsHelper.KEY_PING_TIMEOUT, Const.DEFAULT_PING_TIMEOUT); } }
private void sendLink() { String url = settingsHelper.getString(SettingsHelper.KEY_SERVER_URL); url += "?session=" + sessionId + "&pin=" + password; try { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.send_link_subject)); String shareMessage= getString(R.string.send_link_message, url, settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME)); shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage); startActivity(Intent.createChooser(shareIntent, getString(R.string.send_link_chooser))); } catch(Exception e) { e.printStackTrace(); Toast.makeText(this, R.string.send_link_failed, Toast.LENGTH_LONG).show(); } }
private void showAbout() { ImageView imageView = new ImageView(this); imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher)); new AlertDialog.Builder(this) .setTitle(R.string.about_title) .setMessage(getString(R.string.about_message, BuildConfig.VERSION_NAME, BuildConfig.VARIANT)) .setPositiveButton(R.string.ok, (dialog, which) -> dialog.dismiss()) .create() .show(); }
private void connect() { if (sessionId == null || password == null) { sessionId = Utils.randomString(8, true); password = Utils.randomString(4, true); } sharingEngine.setUsername(settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME)); sharingEngine.connect(this, sessionId, password, (success, errorReason) -> { if (!success) { if (errorReason != null && errorReason.equals(Const.ERROR_ICE_FAILED)) { errorReason = getString(R.string.connection_error_ice); } String message = getString(R.string.connection_error, settingsHelper.getString(SettingsHelper.KEY_SERVER_URL), errorReason); reportError(message); editTextSessionId.setText(null); editTextPassword.setText(null); } });
scheduleExitOnIdle(); }
private void reportError(final String message) {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
final AlertDialog dialog = new AlertDialog.Builder(this) .setMessage(message) .setNegativeButton(R.string.copy_message, (dialog1, which) -> { ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText(Const.LOG_TAG, message); clipboard.setPrimaryClip(clip); Toast.makeText(MainActivity.this, R.string.message_copied, Toast.LENGTH_LONG).show(); dialog1.dismiss(); }) .setPositiveButton(R.string.close, (dialog1, which) -> dialog1.dismiss()) .create(); try { dialog.show(); handler.postDelayed(() -> { try { dialog.dismiss(); } catch (Exception e) { } }, 10000); } catch (Exception e) { } }
@Override public void onStartSharing(String adminName) { // This event is raised when the admin joins the text room this.adminName = adminName; updateUI(); cancelExitOnIdle(); scheduleSharingTimeout(); ScreenSharingHelper.requestSharing(this); }
@Override public void onStopSharing() { // This event is raised when the admin leaves the text room notifySharingStop(); adminName = null; updateUI(); cancelSharingTimeout(); scheduleExitOnIdle(); ScreenSharingHelper.stopSharing(this, false); }
@Override public void onRemoteControlEvent(String event) { Intent intent = new Intent(MainActivity.this, GestureDispatchService.class); intent.setAction(Const.ACTION_GESTURE); intent.putExtra(Const.EXTRA_EVENT, event); startService(intent); }
@Override public void onPing() { if (adminName != null) { cancelSharingTimeout(); scheduleSharingTimeout(); } }
@Override public void onSharingApiStateChanged(int state) { updateUI(); if (state == Const.STATE_CONNECTED) { String rtpHost = Utils.getRtpUrl(settingsHelper.getString(SettingsHelper.KEY_SERVER_URL)); int rtpAudioPort = sharingEngine.getAudioPort(); int rtpVideoPort = sharingEngine.getVideoPort(); String testDstIp = settingsHelper.getString(SettingsHelper.KEY_TEST_DST_IP); if (testDstIp != null && !testDstIp.trim().equals("")) { rtpHost = testDstIp; rtpVideoPort = Const.TEST_RTP_PORT; Toast.makeText(this, "Test mode: sending stream to " + rtpHost + ":" + rtpVideoPort, Toast.LENGTH_LONG).show(); }
ScreenSharingHelper.configure(this, settingsHelper.getBoolean(SettingsHelper.KEY_TRANSLATE_AUDIO), settingsHelper.getInt(SettingsHelper.KEY_FRAME_RATE), settingsHelper.getInt(SettingsHelper.KEY_BITRATE), rtpHost, rtpAudioPort, rtpVideoPort ); } }
private void scheduleExitOnIdle() { int exitOnIdleTimeout = settingsHelper.getInt(SettingsHelper.KEY_IDLE_TIMEOUT); if (exitOnIdleTimeout > 0) { exitCounter = EXIT_PROMPT_SEC; handler.postDelayed(warningOnIdleRunnable, exitOnIdleTimeout * 1000); Log.d(Const.LOG_TAG, "Scheduling exit in " + (exitOnIdleTimeout * 1000) + " sec"); } }
private void cancelExitOnIdle() { Log.d(Const.LOG_TAG, "Cancelling scheduled exit"); handler.removeCallbacks(warningOnIdleRunnable); handler.removeCallbacks(exitRunnable); }
private Runnable exitRunnable = () -> { exitCounter--; if (exitCounter > 0) { TextView messageView = exitOnIdleDialog.findViewById(android.R.id.message); if (messageView != null) { messageView.setText(MainActivity.this.getResources().getString(R.string.app_idle_warning, exitCounter)); } scheduleExitRunnable();
} else { gracefulExit(); } };
private Runnable warningOnIdleRunnable = () -> { exitOnIdleDialog = new AlertDialog.Builder(MainActivity.this) .setMessage(MainActivity.this.getResources().getString(R.string.app_idle_warning, exitCounter)) .setPositiveButton(R.string.button_exit, (dialog1, which) -> { gracefulExit(); }) .setNegativeButton(R.string.button_keep_idle, (dialog1, which) -> { scheduleExitOnIdle(); handler.removeCallbacks(exitRunnable); dialog1.dismiss(); }) .setCancelable(false) .create(); try { exitOnIdleDialog.show(); scheduleExitRunnable(); } catch (Exception e) { gracefulExit(); } };
private void scheduleExitRunnable() { handler.postDelayed(exitRunnable, 1000); }
private void scheduleSharingTimeout() { int pingTimeout = settingsHelper.getInt(SettingsHelper.KEY_PING_TIMEOUT); if (pingTimeout > 0) { Log.d(Const.LOG_TAG, "Scheduling sharing stop in " + (pingTimeout * 1000) + " sec"); handler.postDelayed(sharingStopByPingTimeoutRunnable, pingTimeout * 1000); } }
private void cancelSharingTimeout() { Log.d(Const.LOG_TAG, "Cancelling scheduled sharing stop"); handler.removeCallbacks(sharingStopByPingTimeoutRunnable); }
private Runnable sharingStopByPingTimeoutRunnable = new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, R.string.app_sharing_session_ping_timeout, Toast.LENGTH_LONG).show(); if (adminName != null) { notifySharingStop(); ScreenSharingHelper.stopSharing(MainActivity.this, false); } adminName = null; updateUI(); cancelSharingTimeout(); scheduleExitOnIdle(); sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> connect()); } };
private Runnable overlayDotRunnable = new Runnable() { @Override public void run() { if (overlayDotDirection == 0) { return; } overlayDotAlpha += OVERLAY_DOT_ANIMATION_INCREMENT * overlayDotDirection; if (overlayDotAlpha > 255) { overlayDotAlpha = 255; overlayDotDirection = -overlayDotDirection; } if (overlayDotAlpha < 128) { overlayDotAlpha = 128; overlayDotDirection = -overlayDotDirection; } overlayDot.setImageAlpha(overlayDotAlpha); handler.postDelayed(overlayDotRunnable, OVERLAY_DOT_ANIMATION_DELAY); } };
private void notifySharingStart() { notifyGestureService(Const.ACTION_SCREEN_SHARING_START); if (settingsHelper.getBoolean(SettingsHelper.KEY_NOTIFY_SHARING)) { // Show a flashing dot Utils.lockDeviceRotation(this, true); overlayDot = createOverlayDot(); overlayDotAlpha = 0; overlayDotDirection = 1; handler.postDelayed(overlayDotRunnable, OVERLAY_DOT_ANIMATION_DELAY);
} else { // Just show some dialog to trigger the traffic final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this) .setMessage(R.string.share_start_text) .setPositiveButton(R.string.ok, (dialog1, which) -> dialog1.dismiss()) .create(); dialog.show(); handler.postDelayed(() -> { if (dialog != null && dialog.isShowing()) { try { dialog.dismiss(); } catch (Exception e) { } } }, 3000); } }
private void notifySharingStop() { notifyGestureService(Const.ACTION_SCREEN_SHARING_STOP); if (settingsHelper.getBoolean(SettingsHelper.KEY_NOTIFY_SHARING)) { overlayDotDirection = 0; if (overlayDot != null) { WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); wm.removeView(overlayDot); overlayDot = null; } Utils.lockDeviceRotation(this, false); } }
private void notifyGestureService(String action) { Intent intent = new Intent(MainActivity.this, GestureDispatchService.class); intent.setAction(action); startService(intent); }
public ImageView createOverlayDot() { int size = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_size); WindowManager.LayoutParams params = new WindowManager.LayoutParams(size, size, Utils.OverlayWindowType(), WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); params.gravity = Gravity.LEFT | Gravity.TOP; params.x = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_offset); params.y = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_offset);
ImageView view = new ImageView(this); view.setImageResource(R.drawable.flash_dot); view.setImageAlpha(0); WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE); wm.addView(view, params); return view; }
}